JavaScript · lesson 19 of 32
Beginner
13- 1 Introduction to JavaScript
- 2 Setting Up JavaScript Development Environment
- 3 Variables in JavaScript: var, let, and const
- 4 JavaScript Data Types
- 5 JavaScript Operators
- 6 Control Flow in JavaScript
- 7 Functions in JavaScript
- 8 Working with Strings in JavaScript
- 9 Regular Expressions in JavaScript
- 10 Arrays in JavaScript
- 11 Objects in JavaScript
- 12 Map, Set, WeakMap, and WeakSet in JavaScript
- 13 JSON in JavaScript: stringify, parse, and What Gets Lost
Intermediate
15- 14 Error Handling in JavaScript
- 15 Dates and Times in JavaScript: Date Pitfalls and Temporal
- 16 Object-Oriented Programming in JavaScript
- 17 Prototypes and Prototype Chain in JavaScript
- 18 Closures in JavaScript
- 19 this, call, apply, and bind in JavaScript
- 20 JavaScript Event Loop Explained
- 21 Asynchronous JavaScript: Callbacks, Promises, and Async/Await
- 22 Iterators and Generators in JavaScript
- 23 JavaScript Modules: ESM and CommonJS
- 24 DOM Manipulation in JavaScript
- 25 Browser Web APIs in JavaScript
- 26 Files, Streams, and the Command Line in Node.js
- 27 Testing JavaScript with Jest and Vitest
- 32 JavaScript Interview Preparation: Top 35 Questions
Advanced
4Also part of JavaScript
1this, 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
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
thisis set by the call, not the definition:obj.method()bindsthistoobj; a bare call doesn’t.- Strict mode (ES modules, class bodies,
"use strict") makes a bare call’sthisundefined; sloppy mode falls back toglobalThis. - Detaching a method — assigning it to a variable, passing it to
setTimeout/an event listener/.map()— loses itsthisunless you re-attach it. call(thisArg, ...args)andapply(thisArg, args[])invoke immediately with a chosenthis;bind(thisArg, ...args)returns a new function withthisfixed permanently.- Arrow functions have no
thisof their own and can’t be rebound — they’re the standard fix for a callback that needs the surroundingthis.
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.