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.

Lesson 0125 min read#this#classes#bind

On this page

  1. Words used in this lesson
  2. Part 1. The four rules for this
  3. Part 2. Losing this in callbacks
  4. Part 3. Arrow functions
  5. Part 4. call, apply and bind
  6. Part 5. Classes are sugar over prototypes
  7. Part 6. Fields, private fields, static, extends, super
  8. Part 7. instanceof and mixins
  9. Part 8. Methods passed as callbacks: the fixes
  10. Try it
  11. Interview questions
  12. Key takeaways
  13. 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 is obj.
  • 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 no this of its own.
  • Bound function is a new function made by bind, with this fixed.
  • 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:

#RuleCall looks likethis is
1newnew Fn()the brand new object
2Explicitfn.call(x), fn.apply(x), fn.bind(x)()x
3Implicit (method call)obj.fn()obj (the receiver)
4Defaultfn()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.

JavaScript
"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:

Text
1 plain call: undefined2 method call: obj3 call(): custom4 new: Ctor { made: true }lost: undefined

The 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:

JavaScript
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:

Text
sloppy plain call: globalThissloppy call(5): objectstrict call(5): number

This 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.

JavaScript
"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:

Text
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 = 1

Read it carefully:

  • In Node, a setTimeout callback written as a normal function gets the Timeout object as this. In a browser, it would be window (or undefined in strict mode). Either way, it is not counter, so counter.count stayed 0. This is the classic bug from the setTimeout lesson, now seen from the this side.
  • The arrow function took this from start(), where this was counter2. So it worked.
  • f() is rule 4, so this is undefined and reading .name throws.
  • forEach accepts a second argument thisArg. Most array methods do. It is the old way to fix this.

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.

JavaScript
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:

Text
1 object [ 1 ]true {}A is not a constructorfrom outer1 true
  • o.arrow() returned "object": its this is the module-level this. In a Node CommonJS file that is module.exports (an empty object, as line 2 shows). It is not o. So never use an arrow function as an object method that needs this.
  • In nested, the arrow inside map reuses this of nested, so this.v works. This is the best use of arrows: callbacks inside methods.
  • Arrows do not have their own arguments either: inner saw the arguments of outer.
  • No rule can change an arrow's this. call, apply and bind cannot (the last line: arrowBound() still gave the module this).

Part 4. call, apply and bind

All three set this by hand.

MethodCalls now?ArgumentsReturns
fn.call(thisArg, a, b)yeslisted one by onethe result
fn.apply(thisArg, [a, b])yesin an arraythe result
fn.bind(thisArg, a)nosome now, rest latera new function
JavaScript
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:

Text
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:

  1. bind fixes this for good. A later call(bo, ...) could not change it (line 4). It also fixed the first argument "Hey", so the bound function needs only one more argument: length went from 2 to 1. This is called partial application.
  2. A bound function is named bound intro. This helps in stack traces.
  3. new BoundPt(5) works. For a bound function, the this given to bind is ignored when you call it with new (rule 1 is stronger).
  4. My MyBind is a short teaching version that ignores new. It shows the core idea: bind returns a closure that remembers fn and ctx. This is the closure idea again.
  5. apply was the old way to spread an array into arguments. Today Math.max(...arr) does the same.
  6. call on 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:

JavaScript
class K { m() {} }console.log(typeof K, K.prototype.constructor === K, K.prototype.m === new K().m);console.log(K.toString());

Output:

Text
function true true

and 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:

JavaScript
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:

Text
Rex makes a sound (woof) true true

Old 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.

DifferenceConstructor functionclass
Call without newruns, with a wrong thisthrows an error
Hoistingfunction declaration is hoistedin the temporal dead zone, like let
Methodsenumerable if you assign themnot enumerable
Body modesloppy unless you write italways strict
Private # fieldsnot possibleyes

Part 6. Fields, private fields, static, extends, super

One class using all the features:

JavaScript
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:

Text
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 after super() returns (in a derived class). They are own properties: legs is in the printed object.
  • Private fields (#secret) do not show in the printed object, and not in Object.keys or JSON.stringify. Outside the class, d.#secret is not just undefined: it is a syntax error, found before any code runs. Private methods (#priv) work the same way. #x in obj tests whether an object has the private field.
  • Methods and getters are on Animal.prototype and are not enumerable (Object.keys(Animal.prototype) is empty).
  • static members live on the class function itself (Animal.count, Animal.create), not on instances. A subclass inherits them, because Object.getPrototypeOf(Dog) === Animal.
  • extends sets up two links: Dog.prototype -> Animal.prototype (for instance methods) and Dog -> Animal (for statics). The output shows both are true.
  • super(...) calls the parent constructor. super.speak() calls the parent's version of a method.

Rules and traps in derived classes

JavaScript
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:

Text
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[] {}
  1. In a derived class you must call super() before touching this. The object does not exist until the parent constructor creates it.
  2. new Q().hi is undefined, not "set". The parent constructor ran this.init() before Q's field field was set. Do not call overridable methods from a constructor.
  3. A class is not usable before its declaration line (Later), unlike a function declaration.
  4. Private names are checked when the code is parsed. A misspelt #y is a SyntaxError, which is safer than a missing undefined.

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.

JavaScript
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:

Text
true true true truetrue falsetrue true false{"id":7} true trueUser -> (anon) -> (anon) -> EntityTweety fliestrue 3
  • Object.create(null) instanceof Object is false: 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 call super methods of earlier ones.
  • Object.assign(Bird.prototype, canFly) is the simpler mixin: copy methods onto a prototype. It copies methods, so there is no super link and no instanceof for the mixin.
  • Extending built-ins works: map on an Ex made another Ex.

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:

JavaScript
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:

Text
undefined OK OKtrue false truefalse true[ undefined ]
FixHowCost
Call through the objectel.addEventListener("click", () => b.handleBad())an extra wrapper function
bind in the constructorthis.h = this.h.bind(this)one function per instance
Arrow function fieldh = () => { ... }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

  1. What does this print? const o = { n: 1, f() { return () => this.n; } }; const g = o.f(); const h = { n: 2, g }; then console.log(h.g()).
  2. Fix: class T { x = 5; run() { setTimeout(this.show, 0); } show() { console.log(this.x); } } prints undefined or throws. Give two fixes.
  3. Predict: function f() { return this; } f.call(7) in sloppy mode and in strict mode (typeof of the result).
  4. Why can new not be used on an arrow function?
  5. Write myCall(fn, ctx, ...args) without using call, apply or bind.

Answers:

  1. 1. The arrow was created inside f, when this was o. h.g() does not matter for an arrow. I ran it and got 1.
  2. Fix A: setTimeout(() => this.show(), 0). Fix B: show = () => { console.log(this.x); } (a field), or setTimeout(this.show.bind(this), 0). The original call loses this because setTimeout calls the bare function. I ran the class: in Node, this is the Timeout object, so the original printed undefined. Both fixes printed 5.
  3. Sloppy: "object" (a Number wrapper). Strict: "number". This is the result printed in Part 1.
  4. An arrow has no this of its own and no prototype, so new has nothing to set up. Node says A is not a constructor (printed in Part 3).
  5. 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 makes fn a method of ctx for one call. I ran it: myCall(function (a) { return this.n + a; }, { n: 10 }, 5) gave 15. It throws a TypeError on a frozen ctx and does not work on primitives, which is why the real call is 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

  • this is 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, bind or an arrow field.
  • Arrow functions have no this, arguments or prototype. They use the surrounding this, and nothing can change it.
  • bind fixes this for good and can fix leading arguments. call and apply run now.
  • A class is a function. Its methods are on Class.prototype; extends links both the prototypes and the constructors (for statics).
  • Private #fields are enforced by the language. In derived classes call super() before this, 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

Part of the study guide

Related series