How it works
The spec has no variable called this. The keyword evaluates to ResolveThisBinding(), which calls GetThisEnvironment(): starting from the running execution context's lexical environment, it walks the [[OuterEnv]] chain until an Environment Record answers true to HasThisBinding(). A Function Environment Record answers false only when its [[ThisBindingStatus]] is lexical, which is how arrow functions "inherit" this: they are skipped. A Module Environment Record's GetThisBinding returns undefined; a Global Environment Record returns its [[GlobalThisValue]].
For a normal function call the value is chosen in two steps.
- The call expression.
EvaluateCall receives the reference produced by the part left of the parentheses. If it is a property reference (obj.m, obj[k]), thisValue is the base object. If it is a plain identifier reference, thisValue is undefined, except inside a sloppy with block, where the object environment's WithBaseObject() supplies it. If the callee was not a reference at all (the result of a call, a comma expression, a ||), thisValue is undefined.
- The callee's
[[Call]]. OrdinaryCallBindThis reads [[ThisMode]], fixed when the function was created. lexical: do nothing. strict: bind thisArg as given. Otherwise (global): undefined/null become the callee realm's global this value, and anything else goes through ToObject.
new takes another path: [[Construct]] of a base function creates the object with OrdinaryCreateFromConstructor(newTarget, "%Object.prototype%") and binds it with the same OrdinaryCallBindThis. If the body returns an object, that object wins; otherwise the new object is the result.
The five rules in the answer are therefore one mechanism. new and bound functions override because they replace the thisArg before the callee sees it. call/apply pass thisArg directly. The reference rules produce thisArg when none is passed.
Edge cases
Sloppy versus strict, and the top level, run as CommonJS here because modules are always strict:
// Run as CommonJS: node example.cjs
function sloppy() { return this; }
function strict() { 'use strict'; return this; }
console.log(sloppy() === globalThis); // → true
console.log(strict()); // → undefined
console.log(typeof sloppy.call(5)); // → object
console.log(typeof strict.call(5)); // → number
console.log(this === module.exports); // → true
sloppy.call(5) boxes the primitive; strict.call(5) leaves it alone. Node wraps a CommonJS file in a function, so top-level this is module.exports, not globalThis. An ES module's top level is undefined.
Classes are always strict, so extracting a method gives undefined. A derived constructor's binding starts uninitialised:
class Animal {
static create() { return this; }
speak() { return this; }
}
class Dog extends Animal {
constructor() {
try {
this.legs = 4;
} catch (error) {
console.log(error.name); // → ReferenceError
}
super();
this.legs = 4; // allowed now: super() initialised the binding
}
}
console.log(Animal.create() === Animal); // → true
const { speak } = new Animal();
console.log(speak()); // → undefined
console.log(new Dog().legs); // → 4
A static method's this is the class because the call is Animal.create(); it is still the ordinary reference rule.
Accessors use the same rule, so this is the object the property was read from, even when the getter lives on a prototype:
const counter = {
count: 0,
get next() {
return ++this.count;
},
};
const child = Object.create(counter);
console.log(child.next); // → 1
console.log(Object.hasOwn(child, 'count')); // → true written on `child`
console.log(counter.count); // → 0
Other cases: a [[Call]] on a class constructor throws before binding anything; thisArg passed to a bound function or arrow is ignored (see the call-apply-bind and arrow-functions-this questions); and a direct eval resolves this through the same environment chain as the code around it.
Trade-offs
Call-site binding lets one function serve many receivers: Array.prototype.slice.call(arrayLike) and prototype method sharing depend on it. The price is that this is a hidden argument that vanishes whenever a function is passed around without its receiver, which is the root of lost-this bugs. The standard remedies each change a different rule: an arrow (lexical), bind (fixed), or avoiding this in favour of explicit parameters or closures. Prefer the last where the code does not need a receiver.