Skip to main content
JavaScript · lesson 19 of 32
JavaScript intermediate Lesson 19 of 32

this, call, apply, and bind in JavaScript

Learn how JavaScript's this is bound at call time, why detaching a method loses it, and how call, apply, and bind control it explicitly.

By SkillByExample · Published

javascript this call apply bind

this isn’t fixed when a function is written — it’s set fresh every time the function is called, based on how it was called. That rule, plus the tools that override it, explain most of the “why doesn’t this work here” bugs in JavaScript. Every example runs with node (Node v24.18.1).

this Depends on the Call, Not the Definition

Call a function through an object and this is that object. Call the same kind of function on its own and the result depends on strict mode.

// binding-rules.cjs
const obj = {
  name: "obj",
  whoAmI() { return this.name; },
};

function plainFunction() {
  return this;
}

console.log(obj.whoAmI());
console.log(plainFunction() === globalThis);
$ node binding-rules.cjs
obj
true

obj.whoAmI() prints "obj" because the call is made through obj. plainFunction() is called with nothing in front of it, and this file is sloppy-mode CommonJS, so this defaults to globalThis. The same file as an ES module tells a different story:

// binding-rules.mjs
const obj = {
  name: "obj",
  whoAmI() { return this.name; },
};

function plainFunction() {
  return this;
}

console.log(obj.whoAmI());
console.log(plainFunction());
$ node binding-rules.mjs
obj
undefined

ES modules are always strict mode, and strict mode never falls back to globalThis — an unbound call gets this = undefined. Class bodies are strict mode too, with or without modules, which is why the next example throws instead of silently reading from globalThis.

The Lost Method Reference

Copying a method off an object into a plain variable detaches it from that object. Calling the variable calls the function with no object in front of it.

// lost-this.js
class Counter {
  constructor() { this.count = 0; }
  increment() { this.count++; return this.count; }
}

const counter = new Counter();
console.log(counter.increment());

const incRef = counter.increment;
try {
  incRef();
} catch (err) {
  console.log(err.constructor.name + ":", err.message);
}
$ node lost-this.js
1
TypeError: Cannot read properties of undefined (reading 'count')

counter.increment() works because the call goes through counter. incRef holds the exact same function, but calling it bare — incRef() — has nothing in front of the (, so this is undefined (class bodies are strict mode). this.count++ then tries to read a property off undefined and throws.

setTimeout(obj.method) Has the Same Bug

Passing a method to setTimeout, an event listener, or .map() hands over the bare function — the object it “belongs to” doesn’t travel with it.

// settimeout-loses-this.js
const logger = {
  prefix: "[app]",
  log(msg) { console.log(this.constructor.name + ":", this.prefix, msg); },
};

logger.log("direct call works");

setTimeout(logger.log, 0, "lost inside setTimeout");
setTimeout(() => logger.log("arrow wrapper keeps this"), 0);
setTimeout(logger.log.bind(logger), 0, "bind keeps this too");
$ node settimeout-loses-this.js
Object: [app] direct call works
Timeout: undefined lost inside setTimeout
Object: [app] arrow wrapper keeps this
Object: [app] bind keeps this too

setTimeout(logger.log, ...) calls logger.log later with no object in front of it, so this inside isn’t logger at all — this.constructor.name prints Timeout, Node’s internal timer object, proving exactly what this became. this.prefix reads undefined from that object rather than throwing, which is the quieter and more dangerous version of this bug. Wrapping the call in an arrow function (() => logger.log(...)) re-establishes the call through logger, back to this.constructor.name being Object. logger.log.bind(logger) fixes this permanently on a new function before handing it to setTimeout, with the same result.

call, apply, and bind

All three let you choose this explicitly instead of relying on how the function happens to be called.

// call-apply-bind.js
function introduce(greeting, punctuation) {
  return `${greeting}, I'm ${this.name}${punctuation}`;
}

const alice = { name: "Alice" };
const bob = { name: "Bob" };

console.log(introduce.call(alice, "Hi", "!"));
console.log(introduce.apply(bob, ["Hello", "."]));

const greetAlice = introduce.bind(alice, "Hey");
console.log(greetAlice("?"));
console.log(greetAlice("!!"));

const reboundAlice = greetAlice.bind(bob);
console.log(reboundAlice("!!!"));
$ node call-apply-bind.js
Hi, I'm Alice!
Hello, I'm Bob.
Hey, I'm Alice?
Hey, I'm Alice!!
Hey, I'm Alice!!!

call and apply both invoke introduce right now with the given this — call spreads the remaining arguments individually, apply takes them as one array. bind is different: it doesn’t call anything, it returns a new function (greetAlice) with this and the first argument ("Hey") permanently attached, ready to call later with the rest. The last line proves the binding sticks: calling .bind(bob) on an already-bound function only adds another pre-filled argument — this is still alice.

Arrow Functions Capture this Lexically

An arrow function has no this of its own. It reads this from the scope where it was written, the same way a closure reads any other outer variable — and that reference can’t be detached.

// arrow-this.js
class Clicker {
  constructor() {
    this.clicks = 0;
  }
  // a regular method loses `this` once detached from the instance
  handleClickBroken() {
    this.clicks++;
    return this.clicks;
  }
  // an arrow field captures `this` from the constructor and keeps it
  handleClickFixed = () => {
    this.clicks++;
    return this.clicks;
  };
}

const c = new Clicker();
const broken = c.handleClickBroken;
const fixed = c.handleClickFixed;

try {
  broken();
} catch (err) {
  console.log(err.constructor.name + ":", err.message);
}
console.log(fixed());
console.log(fixed());
$ node arrow-this.js
TypeError: Cannot read properties of undefined (reading 'clicks')
1
2

handleClickBroken is an ordinary method, so detaching it loses this exactly like the counter example above. handleClickFixed is a class field initialized to an arrow function; when that arrow function is created, inside the constructor, this is the new instance — and the arrow keeps that reference forever, regardless of how fixed is later called.

Class Methods Passed as Callbacks

A common pre-arrow-fields idiom does the same fix by hand, inside the constructor, with bind.

// bind-in-constructor.js
class Button {
  constructor(label) {
    this.label = label;
    this.onClick = this.onClick.bind(this);
  }
  onClick() {
    return `clicked: ${this.label}`;
  }
}

const btn = new Button("Save");
const handler = btn.onClick; // detached, but pre-bound in the constructor
console.log(handler());

const handlers = [btn.onClick];
console.log(handlers.map((h) => h()));
$ node bind-in-constructor.js
clicked: Save
[ 'clicked: Save' ]

The constructor replaces this.onClick (the shared method on the prototype) with a bound copy that belongs only to this instance. Every later reference to btn.onClick — detached into handler, stored in an array, passed to .map() — carries this with it, because the binding happened once, up front, rather than relying on every call site to remember.

Key Takeaways

  • this is set by the call, not the definition: obj.method() binds this to obj; a bare call doesn’t.
  • Strict mode (ES modules, class bodies, "use strict") makes a bare call’s this undefined; sloppy mode falls back to globalThis.
  • Detaching a method — assigning it to a variable, passing it to setTimeout/an event listener/.map() — loses its this unless you re-attach it.
  • call(thisArg, ...args) and apply(thisArg, args[]) invoke immediately with a chosen this; bind(thisArg, ...args) returns a new function with this fixed permanently.
  • Arrow functions have no this of their own and can’t be rebound — they’re the standard fix for a callback that needs the surrounding this.

Practice

1. Fix incRef from the lost-method example using bind, and call it twice.
// p1.js
class Counter {
  constructor() { this.count = 0; }
  increment() { this.count++; return this.count; }
}

const counter = new Counter();
const incRef = counter.increment.bind(counter);
console.log(incRef());
console.log(incRef());
$ node p1.js
1
2

bind returns a new function permanently attached to counter, so incRef behaves exactly like calling counter.increment() — including that count keeps increasing across calls, because it’s still the same counter.

2. Remove the bind line from the Button constructor and predict what happens when handler() runs, then run it.
// p2.js
class Button {
  constructor(label) {
    this.label = label;
    // bind line removed on purpose
  }
  onClick() {
    return `clicked: ${this.label}`;
  }
}

const btn = new Button("Save");
const handler = btn.onClick;
try {
  console.log(handler());
} catch (err) {
  console.log(err.constructor.name + ":", err.message);
}
$ node p2.js
TypeError: Cannot read properties of undefined (reading 'label')

Without the constructor’s bind, btn.onClick is the plain prototype method — detaching it into handler loses this the same way the earlier examples did. The constructor’s bind line is the whole fix; nothing about onClick itself needs to change.

3. Use bind to pre-fill a volume(length, width, height) function's leading arguments, two different functions deep.
// p3.js
function volume(length, width, height) {
  return length * width * height;
}

const sheet = volume.bind(null, 10); // pre-fill length only
const base = sheet.bind(null, 4);    // pre-fill width too

console.log(volume(10, 4, 2));
console.log(sheet(4, 2));
console.log(base(2));
$ node p3.js
80
80
80

bind’s extra arguments fill in from the left, and binding again on an already-bound function keeps stacking them — base only needs height because length and width are both locked in. The this argument is null throughout because volume never reads this.

4. Compare Math.max.apply with the modern spread syntax for the same array of numbers.
// p4.js
const nums = [4, 19, 2, 77, 15];

console.log(Math.max.apply(null, nums));
console.log(Math.max(...nums));
$ node p4.js
77
77

Before spread syntax existed, apply was the only way to call a function with an array’s elements as separate arguments — Math.max doesn’t accept an array directly. Math.max(...nums) does the same spreading at the call site and reads more clearly; apply is still around for when this also needs to be set, which spread alone can’t do.

Frequently Asked Questions

How is this determined in JavaScript?
this is set by how a function is called, not where it's defined. Calling obj.method() binds this to obj. Calling the same function detached — fn() — binds this to undefined in strict mode (including inside any class or module) or to globalThis in sloppy-mode scripts. Arrow functions have no this of their own; they read it from the enclosing scope at the point they were written.
What's the difference between call, apply, and bind?
call(thisArg, a, b) and apply(thisArg, [a, b]) both invoke the function immediately with a given this — call takes the extra arguments individually, apply takes them as an array. bind(thisArg, a) does not invoke anything; it returns a new function with this (and optionally some leading arguments) permanently fixed, to be called later.
Why does a class method throw 'Cannot read properties of undefined' when passed as a callback?
A class body is always strict mode, so a method called without an object in front of it (const f = obj.method; f()) gets this = undefined instead of obj. The method then tries to read a property off this and throws, because undefined has no properties.
Does calling bind twice on the same function change its this again?
No. Once a function's this is bound with bind, it cannot be rebound — calling .bind() again on the already-bound function only adds more pre-filled leading arguments; the original this sticks.