JavaScript Part 2
this, call/apply/bind and Classes
The four rules for this, arrow functions, call, apply and bind, and classes shown as sugar over prototypes.
On this page
- Words used in this lesson
- Part 1. The four rules for this
- Part 2. Losing this in callbacks
- Part 3. Arrow functions
- Part 4. call, apply and bind
- Part 5. Classes are sugar over prototypes
- Part 6. Fields, private fields, static, extends, super
- Part 7. instanceof and mixins
- Part 8. Methods passed as callbacks: the fixes
- Try it
- Interview questions
- Key takeaways
- What's next
In one line: this is not decided where a function is written. It is decided where and how the function is called, with one exception: arrow functions, which copy this from the code around them. Once you know that, class is easy to understand, because it is a cleaner way to write the prototype code from Lesson 0.
You will learn the four rules that decide this, why this gets lost when you pass a method as a callback, what call, apply and bind do, and how class, private fields, static, extends and super work underneath. I link to the earlier series for the basics: functions and variable environments, closures and first-class functions and callbacks. For interview practice see JavaScript Core Interview Questions.
What I ran / checked. All code was run with Node v22.22.0 (CommonJS files, run as node file.js) and the output is pasted as printed. Where a result is specific to Node (for example, what this is inside a setTimeout callback), I say so; a browser gives a different answer. Error text is V8's wording. I did not run anything in a browser. I did not re-read the specification for this lesson; every behaviour I state about this and classes was checked by running it.
Words used in this lesson
- Call site is the place in code where a function is called, such as
obj.run(). - Receiver is the object before the dot in a method call. In
obj.run(), the receiver isobj. - Strict mode is a mode (
"use strict", and always on inside modules and classes) that turns some silent mistakes into errors. - Arrow function is a function written
() => .... It has nothisof its own. - Bound function is a new function made by
bind, withthisfixed. - Class field is a property declared inside a class body, such as
count = 0;. - Private field is a field whose name starts with
#. Only code inside the class body can read it. - Mixin is a way to add behaviour from several sources to a class.
Part 1. The four rules for this
Inside a normal function, this is a hidden extra argument. The engine fills it in at the moment of the call. Look at the call site and apply these rules, from the strongest to the weakest:
| # | Rule | Call looks like | this is |
|---|---|---|---|
| 1 | new | new Fn() | the brand new object |
| 2 | Explicit | fn.call(x), fn.apply(x), fn.bind(x)() | x |
| 3 | Implicit (method call) | obj.fn() | obj (the receiver) |
| 4 | Default | fn() | undefined in strict mode, the global object in sloppy mode |
Arrow functions are outside this table: they ignore all four rules and use the this of the place where they were created.
"use strict";function show() { return this === undefined ? "undefined" : this === globalThis ? "globalThis" : (this.name || typeof this); }console.log("1 plain call:", show());const obj = { name: "obj", show };console.log("2 method call:", obj.show());console.log("3 call():", show.call({ name: "custom" }));function Ctor() { this.made = true; }console.log("4 new:", new Ctor());const loose = obj.show;console.log("lost:", loose());Output:
1 plain call: undefined2 method call: obj3 call(): custom4 new: Ctor { made: true }lost: undefinedThe same function show gave four different answers. Nothing about the function changed. Only the call changed. The last line is the key to most this bugs: loose = obj.show copies the function, not the link to obj. The call loose() has no receiver, so rule 4 applies.
Sloppy mode and primitives
Outside strict mode, the default rule uses the global object, and call with a primitive wraps it in an object:
function show() { return this === globalThis ? "globalThis" : typeof this; }console.log("sloppy plain call:", show());console.log("sloppy call(5):", (function () { return typeof this; }).call(5));console.log("strict call(5):", (function () { "use strict"; return typeof this; }).call(5));Output:
sloppy plain call: globalThissloppy call(5): objectstrict call(5): numberThis is why sloppy-mode this bugs are nasty: instead of an error, you silently read or write global variables. (The global object is covered in Window and globalThis.) Classes and ES modules are always strict, so you get an error instead.
Part 2. Losing this in callbacks
When you pass obj.method as a callback, you pass the function only. The one who calls it later (setTimeout, forEach, an event system) does not know about obj.
"use strict";const counter = { count: 0, start() { setTimeout(function () { console.log("regular fn this:", this.constructor.name, "| this === counter?", this === counter); this.count++; // changes the Timeout object, not counter console.log("counter.count is still", counter.count); }, 0); },};counter.start();const counter2 = { count: 0, start() { setTimeout(() => { this.count++; console.log("arrow: count =", this.count); }, 0); },};counter2.start();const user = { name: "Ana", hello() { return "hi " + this.name; } };const f = user.hello;try { console.log(f()); } catch (e) { console.log("detached:", e.message); }[1].forEach(function () { console.log("forEach fn this:", this); });[1].forEach(function () { console.log("forEach with thisArg:", this); }, { thisArg: true });Output:
detached: Cannot read properties of undefined (reading 'name')forEach fn this: undefinedforEach with thisArg: { thisArg: true }regular fn this: Timeout | this === counter? falsecounter.count is still 0arrow: count = 1Read it carefully:
- In Node, a
setTimeoutcallback written as a normal function gets theTimeoutobject asthis. In a browser, it would bewindow(orundefinedin strict mode). Either way, it is notcounter, socounter.countstayed0. This is the classic bug from the setTimeout lesson, now seen from thethisside. - The arrow function took
thisfromstart(), wherethiswascounter2. So it worked. f()is rule 4, sothisisundefinedand reading.namethrows.forEachaccepts a second argumentthisArg. Most array methods do. It is the old way to fixthis.
Part 3. Arrow functions
An arrow function has no this, no arguments, no prototype, and cannot be used with new. When its body uses this, the engine looks it up like a variable, through the scope chain, in the function that surrounds it.
const o = { v: 1, regular() { return this?.v; }, arrow: () => typeof this, nested() { return [1].map(() => this.v); } };console.log(o.regular(), o.arrow(), o.nested());console.log(this === module.exports, this);const A = () => {};try { new A(); } catch (e) { console.log(e.message); }function outer() { const inner = () => arguments[0]; return inner("ignored"); }console.log(outer("from outer"));const bound = function () { return this.n; }.bind({ n: 1 });const arrowBound = (() => this).bind({ n: 2 });console.log(bound.call({ n: 99 }), arrowBound() === module.exports);Output:
1 object [ 1 ]true {}A is not a constructorfrom outer1 trueo.arrow()returned"object": itsthisis the module-levelthis. In a Node CommonJS file that ismodule.exports(an empty object, as line 2 shows). It is noto. So never use an arrow function as an object method that needsthis.- In
nested, the arrow insidemapreusesthisofnested, sothis.vworks. This is the best use of arrows: callbacks inside methods. - Arrows do not have their own
argumentseither:innersaw theargumentsofouter. - No rule can change an arrow's
this.call,applyandbindcannot (the last line:arrowBound()still gave the modulethis).
Part 4. call, apply and bind
All three set this by hand.
| Method | Calls now? | Arguments | Returns |
|---|---|---|---|
fn.call(thisArg, a, b) | yes | listed one by one | the result |
fn.apply(thisArg, [a, b]) | yes | in an array | the result |
fn.bind(thisArg, a) | no | some now, rest later | a new function |
function intro(greeting, punct) { return `${greeting}, I am ${this.name}${punct}`; }const ana = { name: "Ana" }, bo = { name: "Bo" };console.log(intro.call(ana, "Hi", "!"));console.log(intro.apply(bo, ["Hello", "?"]));const hiAna = intro.bind(ana, "Hey");console.log(hiAna("."));console.log(hiAna.call(bo, ":)")); // bind wins over callconsole.log(hiAna.name, hiAna.length, intro.length);console.log(Math.max.apply(null, [3, 9, 4]), Math.max(...[3, 9, 4]));function MyBind(fn, ctx, ...pre) { return function (...rest) { return fn.apply(ctx, [...pre, ...rest]); }; }console.log(MyBind(intro, ana, "Yo")("~"));function Pt(x) { this.x = x; }const BoundPt = Pt.bind({ ignored: 1 });console.log(new BoundPt(5));console.log(Array.prototype.slice.call({ 0: "a", 1: "b", length: 2 }));Output:
Hi, I am Ana!Hello, I am Bo?Hey, I am Ana.Hey, I am Ana:)bound intro 1 29 9Yo, I am Ana~Pt { x: 5 }[ 'a', 'b' ]Points from the trace:
bindfixesthisfor good. A latercall(bo, ...)could not change it (line 4). It also fixed the first argument"Hey", so the bound function needs only one more argument:lengthwent from 2 to 1. This is called partial application.- A bound function is named
bound intro. This helps in stack traces. new BoundPt(5)works. For a bound function, thethisgiven tobindis ignored when you call it withnew(rule 1 is stronger).- My
MyBindis a short teaching version that ignoresnew. It shows the core idea:bindreturns a closure that remembersfnandctx. This is the closure idea again. applywas the old way to spread an array into arguments. TodayMath.max(...arr)does the same.callon a borrowed method makes array methods work on array-like objects (last line).
Part 5. Classes are sugar over prototypes
A class looks different from a constructor function, but inspection shows it is the same machinery:
class K { m() {} }console.log(typeof K, K.prototype.constructor === K, K.prototype.m === new K().m);console.log(K.toString());Output:
function true trueand the second console.log printed class K { m() {} }. A class is a function. Its methods live on K.prototype, shared by all instances, exactly like the hand-written version from Lesson 0. Here is the old way and the new way side by side, with inheritance:
function Animal(name) { this.name = name; }Animal.prototype.speak = function () { return this.name + " makes a sound"; };function Dog(name) { Animal.call(this, name); }Dog.prototype = Object.create(Animal.prototype);Dog.prototype.constructor = Dog;Dog.prototype.speak = function () { return Animal.prototype.speak.call(this) + " (woof)"; };const d = new Dog("Rex");console.log(d.speak(), d instanceof Animal, d.constructor === Dog);Output:
Rex makes a sound (woof) true trueOld inheritance needs three steps by hand: call the parent with Animal.call(this, name), link the prototypes with Object.create, and repair constructor. The class syntax does all of this for you. It is still not only sugar: classes add some real differences.
| Difference | Constructor function | class |
|---|---|---|
Call without new | runs, with a wrong this | throws an error |
| Hoisting | function declaration is hoisted | in the temporal dead zone, like let |
| Methods | enumerable if you assign them | not enumerable |
| Body mode | sloppy unless you write it | always strict |
Private # fields | not possible | yes |
Part 6. Fields, private fields, static, extends, super
One class using all the features:
class Animal { legs = 4; // public field #secret = "hidden"; // private field static count = 0; constructor(name) { this.name = name; Animal.count++; } speak() { return `${this.name} makes a sound`; } get secretLen() { return this.#secret.length; } #priv() { return "private method"; } callPriv() { return this.#priv(); } static create(n) { return new Animal(n); }}class Dog extends Animal { constructor(name) { super(name); this.tricks = []; } speak() { return super.speak() + " (woof)"; }}const d = new Dog("Rex");console.log(d.speak(), d.legs, d.secretLen, d.callPriv());console.log(d, Animal.count, Animal.create("x").name);console.log(typeof Animal, Object.getOwnPropertyNames(Animal.prototype));console.log(Object.getPrototypeOf(Dog) === Animal, Object.getPrototypeOf(Dog.prototype) === Animal.prototype);console.log(Object.keys(Animal.prototype), Object.getOwnPropertyDescriptor(Animal.prototype, "speak").enumerable);try { Animal("x"); } catch (e) { console.log(e.message); }Output:
Rex makes a sound (woof) 4 6 private methodDog { legs: 4, name: 'Rex', tricks: [] } 1 xfunction [ 'constructor', 'speak', 'secretLen', 'callPriv' ]true true[] falseClass constructor Animal cannot be invoked without 'new'What each piece does:
- Fields (
legs = 4) are created on each instance, just before the constructor body runs (in a base class) or right aftersuper()returns (in a derived class). They are own properties:legsis in the printed object. - Private fields (
#secret) do not show in the printed object, and not inObject.keysorJSON.stringify. Outside the class,d.#secretis not justundefined: it is a syntax error, found before any code runs. Private methods (#priv) work the same way.#x in objtests whether an object has the private field. - Methods and getters are on
Animal.prototypeand are not enumerable (Object.keys(Animal.prototype)is empty). staticmembers live on the class function itself (Animal.count,Animal.create), not on instances. A subclass inherits them, becauseObject.getPrototypeOf(Dog) === Animal.extendssets up two links:Dog.prototype -> Animal.prototype(for instance methods) andDog -> Animal(for statics). The output shows both aretrue.super(...)calls the parent constructor.super.speak()calls the parent's version of a method.
Rules and traps in derived classes
class A { #x = 1; static has(o) { return #x in o; } }console.log(A.has(new A()), A.has({}));try { new Function("class B { #x; m(){ return this.#y; } }")(); } catch (e) { console.log(e.name + ": " + e.message); }class P { constructor() { this.hi = this.init(); } init() { return "P"; } }class Q extends P { field = "set"; init() { return this.field; } }console.log(new Q().hi);class R extends P { constructor() { try { this.x = 1; } catch (e) { console.log(e.name + ": " + e.message); } super(); } }new R();try { new Later(); class Later {} } catch (e) { console.log(e.name + ": " + e.message); }console.log(Object.getOwnPropertyNames(new A()), JSON.stringify(new A()));Output:
true falseSyntaxError: Private field '#y' must be declared in an enclosing classundefinedReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructorReferenceError: Cannot access 'Later' before initialization[] {}- In a derived class you must call
super()before touchingthis. The object does not exist until the parent constructor creates it. new Q().hiisundefined, not"set". The parent constructor ranthis.init()beforeQ's fieldfieldwas set. Do not call overridable methods from a constructor.- A class is not usable before its declaration line (
Later), unlike a function declaration. - Private names are checked when the code is parsed. A misspelt
#yis aSyntaxError, which is safer than a missingundefined.
Private fields are real privacy, unlike the _name convention. Even Object.getOwnPropertyNames cannot see them (it printed []). The price: they are not visible to JSON.stringify or to subclasses.
Part 7. instanceof and mixins
a instanceof B is true when B.prototype is somewhere on the prototype chain of a (the same walk as in Lesson 0). It does not check who made the object, and a class can customise it with Symbol.hasInstance.
JavaScript has single inheritance: a class has one parent. A mixin adds behaviour from several sources. The common trick is a function that takes a base class and returns a new class that extends it.
class Shape { }class Circle extends Shape { }const c = new Circle();console.log(c instanceof Circle, c instanceof Shape, c instanceof Object, Shape.prototype.isPrototypeOf(c));class Even { static [Symbol.hasInstance](n) { return n % 2 === 0; } }console.log(2 instanceof Even, 3 instanceof Even);console.log([] instanceof Array, Array.isArray([]), Object.create(null) instanceof Object);const Serializable = (Base) => class extends Base { toJSONString() { return JSON.stringify(this); } };const Comparable = (Base) => class extends Base { equals(o) { return this.id === o.id; } };class Entity { constructor(id) { this.id = id; } }class User extends Serializable(Comparable(Entity)) {}const u = new User(7);console.log(u.toJSONString(), u.equals(new User(7)), u instanceof Entity);let p = Object.getPrototypeOf(u), names = [];while (p && p !== Object.prototype) { names.push(p.constructor.name || "(anon)"); p = Object.getPrototypeOf(p); }console.log(names.join(" -> "));const canFly = { fly() { return this.name + " flies"; } };class Bird { constructor(n) { this.name = n; } }Object.assign(Bird.prototype, canFly);console.log(new Bird("Tweety").fly());class Ex extends Array {}const ex = Ex.from([1,2,3]);console.log(ex.map(x => x * 2) instanceof Ex, ex.length);Output:
true true true truetrue falsetrue true false{"id":7} true trueUser -> (anon) -> (anon) -> EntityTweety fliestrue 3Object.create(null) instanceof Objectisfalse: that object has no chain at all.- The mixin chain is real:
User -> (anon) -> (anon) -> Entity. Each mixin function made a new anonymous class in the middle. Order matters: later mixins can callsupermethods of earlier ones. Object.assign(Bird.prototype, canFly)is the simpler mixin: copy methods onto a prototype. It copies methods, so there is nosuperlink and noinstanceoffor the mixin.- Extending built-ins works:
mapon anExmade anotherEx.
Part 8. Methods passed as callbacks: the fixes
A class method is shared on the prototype and has no fixed this. Passing it around breaks it, exactly as in Part 2:
class Btn { label = "OK"; handleBad() { return this?.label; } handleArrowField = () => this.label; constructor() { this.handleBound = this.handleBad.bind(this); }}const b = new Btn();const { handleBad, handleArrowField, handleBound } = b;console.log(handleBad(), handleArrowField(), handleBound());console.log(Object.hasOwn(b, "handleArrowField"), Object.hasOwn(b, "handleBad"), Object.hasOwn(Btn.prototype, "handleBad"));console.log(new Btn().handleArrowField === new Btn().handleArrowField, new Btn().handleBad === new Btn().handleBad);console.log(["a"].map(b.handleBad));Output:
undefined OK OKtrue false truefalse true[ undefined ]| Fix | How | Cost |
|---|---|---|
| Call through the object | el.addEventListener("click", () => b.handleBad()) | an extra wrapper function |
bind in the constructor | this.h = this.h.bind(this) | one function per instance |
| Arrow function field | h = () => { ... } | one function per instance, and it is not on the prototype, so subclasses cannot use super.h() |
The second and third rows show a trade-off: handleArrowField is an own property, so every instance owns a separate function (false for equality), while normal methods are shared (true). That is fine for a few objects. For thousands of instances, prefer a wrapper at the call site.
Try it
- What does this print?
const o = { n: 1, f() { return () => this.n; } }; const g = o.f(); const h = { n: 2, g };thenconsole.log(h.g()). - Fix:
class T { x = 5; run() { setTimeout(this.show, 0); } show() { console.log(this.x); } }printsundefinedor throws. Give two fixes. - Predict:
function f() { return this; } f.call(7)in sloppy mode and in strict mode (typeofof the result). - Why can
newnot be used on an arrow function? - Write
myCall(fn, ctx, ...args)without usingcall,applyorbind.
Answers:
1. The arrow was created insidef, whenthiswaso.h.g()does not matter for an arrow. I ran it and got1.- Fix A:
setTimeout(() => this.show(), 0). Fix B:show = () => { console.log(this.x); }(a field), orsetTimeout(this.show.bind(this), 0). The original call losesthisbecausesetTimeoutcalls the bare function. I ran the class: in Node,thisis theTimeoutobject, so the original printedundefined. Both fixes printed5. - Sloppy:
"object"(aNumberwrapper). Strict:"number". This is the result printed in Part 1. - An arrow has no
thisof its own and noprototype, sonewhas nothing to set up. Node saysA is not a constructor(printed in Part 3). - One solution:
function myCall(fn, ctx, ...args) { const k = Symbol(); ctx[k] = fn; const r = ctx[k](...args); delete ctx[k]; return r; }. It uses rule 3: it makesfna method ofctxfor one call. I ran it:myCall(function (a) { return this.n + a; }, { n: 10 }, 5)gave15. It throws aTypeErroron a frozenctxand does not work on primitives, which is why the realcallis built into the engine.
Interview questions
How is this decided? (Must know)
By the call, not the definition. I check the call site: new gives a new object; call, apply or bind give the object I pass; obj.fn() gives obj; a plain fn() gives undefined in strict mode or the global object in sloppy mode. Arrow functions are the exception: they use the this of the code where they were created.
Why does const f = obj.method; f() lose this? (Must know)
Copying the method copies only the function. The call f() has no receiver, so the default rule applies. In strict code (and in every class), this is undefined, and this.x throws a TypeError.
call vs apply vs bind? (Must know)
call runs now with arguments listed. apply runs now with arguments in an array. bind does not run; it returns a new function with this (and maybe the first arguments) fixed. A bound function cannot be re-bound, but new can still override it.
How do arrow functions treat this? When should you not use them? (Must know)
They have no own this; they read it from the surrounding scope, like a variable. That is perfect for callbacks inside methods. I do not use them for object methods, prototype methods or constructors, because I need a this that depends on the receiver. call and bind cannot change an arrow's this.
Is a class just sugar for prototypes? (Must know)
Mostly. typeof a class is "function", methods go on Class.prototype, and extends links prototypes. But classes also add real differences: they throw if called without new, they are not hoisted usably, they are always strict, their methods are not enumerable, and they support private # fields. I would say "mostly sugar, with a few safety features and private fields that functions cannot copy".
What is the difference between #private and private? (Senior signal)
_name is only a convention: anyone can read it. #name is enforced by the language: outside the class it is a syntax error, and it does not appear in Object.keys, JSON.stringify, or getOwnPropertyNames. Private fields are per class, so a subclass cannot read them either.
What happens if a derived constructor uses this before super()? (Trap)
A ReferenceError. The parent constructor creates the object, so this does not exist until super() returns. A related trap: a field in the child is not set yet when the parent constructor runs, so calling an overridable method from the parent constructor can see undefined.
How do you fix a method passed as a callback? (Must know)
Three ways: wrap it (() => obj.method()), bind it (obj.method.bind(obj)), or define it as an arrow function field. I choose a wrapper at the call site for one-off uses. I choose an arrow field when many callers need the same stable function, and accept one function per instance.
Follow-up: "Why does bind in a constructor matter for removing event listeners?" removeEventListener needs the same function reference that was added. A fresh .bind() each time creates a different function, so it does not remove anything.
What does instanceof really check? (Trap)
Whether Constructor.prototype appears on the prototype chain of the left value. It does not look at the constructor name. It can be fooled by changing prototypes, it breaks across iframes (each frame has its own Array), and it can be customised with Symbol.hasInstance. For arrays I use Array.isArray.
Key takeaways
thisis set by the call:new, then explicit (call,apply,bind), then receiver (obj.fn()), then default.- A method copied out of its object loses its receiver. Fix it with a wrapper,
bindor an arrow field. - Arrow functions have no
this,argumentsorprototype. They use the surroundingthis, and nothing can change it. bindfixesthisfor good and can fix leading arguments.callandapplyrun now.- A class is a function. Its methods are on
Class.prototype;extendslinks both the prototypes and the constructors (for statics). - Private
#fieldsare enforced by the language. In derived classes callsuper()beforethis, and do not call overridable methods in constructors. - Mixins are functions that return a class extending a base.
What's next
Next: Promises and async/await From the Ground Up, Lesson 2. Practise: take any class you wrote and print Object.getPrototypeOf for the instance, the class and the parent class. Draw both chains on paper.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Objects, Interfaces, Type Aliases and Classes · TypeScript From Zero
Part of the study guide
Related series