this and Function Binding
Prerequisites: you should be comfortable with function declarations, arrow functions, object literals, and lexical scope from earlier chapters.
this is not defined by where a function is written but by how it is called. Four binding rules apply, in priority order:
- new binding: a function invoked with
newgetsthisset to a freshly created object. - explicit binding:
call,apply, orbindsetthisto the first argument. - implicit binding:
obj.method()bindsthistoobj. - default binding: a bare call gets
thisasundefined(strict mode) or the global object (sloppy mode).
Arrows are the exception: they have no this at all and inherit it lexically from the enclosing scope. call, apply, and bind on an arrow are silently ignored with respect to this.
'use strict';
function shout() { return this && this.tag || 'no context'; }
const obj = { tag: 'obj', shout, arrow: () => 'arrow: ' + (this === undefined) };
shout(); // 'no context' (default binding)
obj.shout(); // 'obj' (implicit binding)
shout.call({ tag: 'called' }); // 'called' (explicit binding)Where bindings break
The classic bug: extracting a method or passing it as a callback detaches it from its receiver.
const timer = {
label: 'timer',
run() {
setTimeout(function () {
console.log(this.label); // undefined: default binding inside callback
}, 10);
setTimeout(() => {
console.log(this.label); // 'timer': arrow inherits this from run()
}, 20);
},
};
timer.run();The non-arrow callback loses its receiver at the call site; the arrow never had one to lose.
bind, call, apply
call(thisArg, a, b)— invoke now, arguments listed individually.apply(thisArg, [a, b])— invoke now, arguments as an array.bind(thisArg)— return a new function permanently bound (a "hard binding").
A bound function's binding is final: further call on it cannot override this. Note also that bind is partial application of the leading arguments, a technique worth knowing in its own right.
const bound = shout.bind({ tag: 'hard' });
bound.call({ tag: 'soft' }); // 'hard' — bind winsClass internals
Inside a class constructor, this is the new instance. Methods live on Class.prototype, so this resolves through the prototype chain at call time. This is why the same detachment problem affects class methods — and why React class components historically bound handlers in constructors, before arrow class fields became idiomatic.
diagram mermaid caption: How this is resolved for a given call alt: Flowchart starting from a call site: if new was used, this is the new object; else if call, apply, or bind was used, this is the provided argument; else if called as obj.method, this is obj; else if the function is an arrow, this comes from the enclosing scope; otherwise default binding applies, undefined in strict mode.
flowchart TD
A[Call site] --> B{new?}
B -- yes --> C[this = new object]
B -- no --> D{call / apply / bind?}
D -- yes --> E[this = first argument]
D -- no --> F{obj.method()?}
F -- yes --> G[this = obj]
F -- no --> H{arrow function?}
H -- yes --> I[this = enclosing lexical this]
H -- no --> J[default: undefined strict / global sloppy]Gotchas
- Arrow methods defined as class fields share one
thisper instance but avoid detachment; prototype methods do not. Choose based on whether the method is passed around. thisin top-level module code isundefinedunder ES modules, regardless of strict mode.- Debugging detachment is easier if you ask "who is on the left of the dot at the call site?" — for detached calls, nobody.
Exercise
Why does const f = obj.method; f(); lose the receiver, and give two ways to fix it?
Show answer
Assigning the method to f and calling it bare applies default binding, so this is not obj. Fix by wrapping: f = () => obj.method() (restores implicit binding), or by making the method an arrow-bound field, or f = obj.method.bind(obj).
Spotted a mistake, or code that does not work?