Most names in JavaScript are resolved lexically, by reading the code around them. The keyword this is the big exception: for ordinary functions it is decided at call time, by how the function is invoked. That is why the same method can work on one line and throw on the next. Four rules cover almost every case, one ordering settles conflicts between them, and arrow functions opt out of the system entirely. Learn those and every puzzle about it becomes mechanical.
Unless a snippet says otherwise, examples run as ES modules, which are always strict mode (class bodies are strict too). Where sloppy mode or the browser behaves differently, it is called out.
The four binding rules
1. Default binding
A plain call, fn(), with nothing in front of it. In sloppy mode this falls back to the global object (window in a browser script, globalThis in general). In strict mode it is undefined.
// Run as a sloppy-mode script (classic browser script or CommonJS file)
function sloppy() {
return this;
}
function strict() {
'use strict';
return this;
}
console.log(sloppy() === globalThis); // → true (only in sloppy-mode code)
console.log(strict()); // → undefinedTop-level this (outside any function) is its own case: window in a classic browser script, undefined in an ES module, and module.exports (initially {}) in a Node CommonJS file.
2. Implicit binding
Call a function as a property of an object, obj.fn(), and this is that object. Only the object immediately before the call counts.
const user = {
name: 'Ada',
greet() {
return `Hi, ${this.name}`;
},
team: {
name: 'Core',
greet() {
return `Hi from ${this.name}`;
},
},
};
console.log(user.greet()); // → Hi, Ada
console.log(user.team.greet()); // → Hi from Core (only the last object counts)
const greet = user.greet; // just a function now, no object attached
try {
greet();
} catch (e) {
console.log(e.message); // → Cannot read properties of undefined (reading 'name')
}The method is not “owned” by user. Once you copy the function into a variable and call it plainly, the default rule applies. In a sloppy browser script the last call would not even throw: it returns "Hi, ", because this is window and window.name is an empty string. That silence is how these bugs survive.
3. Explicit binding: call, apply, bind
call and apply invoke a function with the this you pass; they differ only in how arguments are supplied. bind returns a new function with this (and optionally leading arguments) fixed permanently.
function introduce(greeting, punctuation) {
return `${greeting}, I'm ${this.name}${punctuation}`;
}
const ada = { name: 'Ada' };
const grace = { name: 'Grace' };
console.log(introduce.call(ada, 'Hi', '!')); // → Hi, I'm Ada!
console.log(introduce.apply(ada, ['Hey', '.'])); // → Hey, I'm Ada.
const adaSaysHello = introduce.bind(ada, 'Hello'); // `this` and first arg fixed
console.log(adaSaysHello('?')); // → Hello, I'm Ada?
console.log(adaSaysHello.call(grace, '?')); // → Hello, I'm Ada? (bind wins)
console.log(adaSaysHello.bind(grace)('!')); // → Hello, I'm Ada! (first bind wins)Passing null or undefined as the this argument gives you the global object in sloppy mode and exactly null or undefined in strict mode. Likewise, a primitive such as 5 is boxed into an object in sloppy mode but stays a primitive in strict mode.
4. new binding
Calling a function with new creates a fresh object, links it to the function’s prototype, runs the function with this set to that object, and returns it (unless the function explicitly returns a different object). The full sequence is in prototypes and inheritance.
function Person(name) {
this.name = name;
}
const ada = new Person('Ada');
console.log(ada.name); // → AdaPrecedence when rules collide
When more than one rule could apply, the order is new > explicit (bind, call, apply) > implicit > default.
function getName() {
return this.name;
}
const a = { name: 'a', getName };
const b = { name: 'b' };
console.log(a.getName()); // → a (implicit)
console.log(a.getName.call(b)); // → b (explicit beats implicit)
const bound = getName.bind(a);
const c = { name: 'c', bound };
console.log(c.bound()); // → a (bind beats implicit)
function Person(name) {
this.name = name;
}
const BoundPerson = Person.bind({ name: 'ignored' });
console.log(new BoundPerson('Grace').name); // → Grace (new beats bind)new on a bound function ignores the bound this but keeps any bound arguments. In practice you rarely rely on that; it simply settles the ordering.
Interview tip
Walk the checklist out loud: “Is it an arrow function? Then it’s the outer
this. Called withnew? The new object.call,applyor a bound function? The object passed in. Called asobj.method()?obj. Otherwise,undefinedin strict mode or the global object in sloppy mode.”
Arrow functions: the exception
Arrow functions have no this of their own. They look it up lexically, like any other variable, from the scope they were defined in. None of the four rules applies: call, apply and bind still pass arguments but cannot change this, and new throws. That makes arrows ideal for callbacks inside methods:
const timer = {
seconds: 0,
start() {
// the arrow has no `this` of its own; it uses start()'s `this`, i.e. timer
const id = setInterval(() => {
this.seconds += 1;
console.log(this.seconds);
if (this.seconds === 3) clearInterval(id);
}, 100);
},
};
timer.start(); // → 1 2 3It also makes them the wrong choice for methods themselves:
const obj = {
regular() {
return this === obj;
},
arrow: () => this === obj, // `this` comes from the module scope, not obj
};
console.log(obj.regular()); // → true
console.log(obj.arrow()); // → false
const arrow = () => this;
console.log(arrow.call({ name: 'ignored' })); // → undefined (in an ES module)
try {
new arrow();
} catch (e) {
console.log(e.message); // → arrow is not a constructor
}Gotcha
An object literal’s braces do not create a scope. An arrow function defined as a property takes
thisfrom wherever the object literal is written (the module, the global scope, or an enclosing function), never from the object.
Where this gets lost
Passing methods as callbacks
counter.increment evaluates to the function alone. Whoever calls it later decides this.
class Counter {
count = 0;
increment() {
this.count += 1;
}
}
const counter = new Counter();
try {
[1, 2, 3].forEach(counter.increment); // passes the function, not the object
} catch (e) {
console.log(e.message); // → Cannot read properties of undefined (reading 'count')
}
[1, 2, 3].forEach(() => counter.increment()); // call it on the object
console.log(counter.count); // → 3Timers are sneakier because they do not throw. setTimeout(counter.increment, 0) calls the function with this set to window in browsers (even for strict-mode code) and to a Timeout object in Node. Either way, the count silently never changes.
Class methods as event handlers
addEventListener calls the listener with this set to the element it is attached to (event.currentTarget), not your instance:
// EventTarget works the same in Node 22 and the browser
class Toggle {
isOn = false;
constructor(button) {
button.addEventListener('click', this.handleClick);
}
handleClick() {
// `this` is the button (event.currentTarget), not the Toggle
this.isOn = !this.isOn;
}
}
const button = new EventTarget();
const toggle = new Toggle(button);
button.dispatchEvent(new Event('click'));
console.log(toggle.isOn); // → false (unchanged)
console.log(button.isOn); // → true (oops: a new property on the button)There are three standard fixes. Declare the handler as an arrow class field (handleClick = () => { ... }), bind it once in the constructor (this.handleClick = this.handleClick.bind(this)), or wrap it at the call site (addEventListener('click', () => this.handleClick())). The first two create one function per instance; that is fine for components, less so for thousands of small objects. Arrow fields also live on the instance rather than the prototype, so subclasses cannot call them with super.handleClick() and tests cannot stub them on the prototype.
Predict the output
Each snippet runs as an ES module.
const obj = {
whoAmI() {
return this === obj ? 'obj' : String(this);
},
};
const f = obj.whoAmI;
console.log(obj.whoAmI()); // 1
console.log((obj.whoAmI)()); // 2
console.log((0, obj.whoAmI)()); // 3
console.log(f()); // 4Answer: obj, obj, undefined, undefined. Parentheses around a property access keep the object reference, so (2) is still a method call. The comma operator in (3) evaluates to the bare function value, which drops the object; that is why compiled code sometimes contains (0, fn)() when it deliberately wants no this.
const team = {
name: 'Core',
members: ['Ada', 'Linus'],
withArrow() {
return this.members.map((m) => `${m}@${this.name}`);
},
withFunction() {
return this.members.map(function (m) {
return `${m}@${this.name}`;
});
},
};
console.log(team.withArrow()); // 1
console.log(team.withFunction()); // 2Answer: (1) logs [ 'Ada@Core', 'Linus@Core' ]. (2) throws TypeError: Cannot read properties of undefined (reading 'name'), because map calls the regular function with the default binding. Passing this as map’s second argument (thisArg) or using an arrow fixes it.
const a = { name: 'a', hello() { return this.name; } };
const b = { name: 'b' };
const bound = a.hello.bind(b);
b.hello = bound;
const c = { name: 'c', hello: b.hello };
console.log(a.hello(), b.hello(), c.hello(), bound.call(a));Answer: a b b b. Only the first call uses implicit binding. Every other call goes through the bound function, and neither a method call nor call can override bind.
The interview answer
“For regular functions, this is determined by the call site, and I check four rules in order of precedence. If the function is called with new, this is the newly created object. If it’s called through call, apply or a bound function, it’s the object that was passed in. If it’s called as a method, obj.method(), it’s the object before the dot. Otherwise it’s the default binding: undefined in strict mode, the global object in sloppy mode.
Arrow functions are the exception: they don’t have their own this, so they use the this of the scope they were defined in, and call, apply or bind can’t change it. Most real bugs come from detaching a method, such as passing obj.method as a callback or event handler. The fixes are an arrow wrapper, bind, or an arrow class field.”