this: 5 Binding Rules, Priority Order, and ResolveThisBinding
The value of this is determined by how a function is called, not where it is defined—arrow functions are the only exception
this is the mechanism in JavaScript that confuses developers most frequently. The core reason is a counter-intuitive design: this is not determined when a function is defined; it is determined at call time according to how the function is invoked. The only exception is arrow functions—they have no this of their own, and instead inherit this from the outer lexical environment at the point of definition, unchanged by any calling convention.
At the Execution Context level, the specification stores the this value in the ThisBinding state component and retrieves the currently effective this through the ResolveThisBinding abstract operation. Mastering the 5 this binding rules and their priority order resolves more than 90% of this-related confusion.
🔹 Level 1 · What You Need to Know
The 5 Binding Rules
Rule 1: Default Binding
Applies when a function is called as a plain invocation with no modifier:
- Non-strict mode:
this= the global object (windowin a browser,globalin Node.js) - Strict mode (
'use strict'):this=undefined
function foo() {
console.log(this);
}
foo(); // window (non-strict) or undefined (strict)
Rule 2: Implicit Binding
When a method is called through an object, this is the object at the call site (the object to the left of .):
const obj = {
name: 'Alice',
greet() {
console.log(this.name);
}
};
obj.greet(); // 'Alice' (this = obj)
Rule 3: Explicit Binding
Using call, apply, or bind to explicitly specify this:
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const alice = { name: 'Alice' };
const bob = { name: 'Bob' };
greet.call(alice, 'Hello'); // 'Hello, Alice'
greet.apply(bob, ['Hi']); // 'Hi, Bob'
const boundGreet = greet.bind(alice);
boundGreet('Hey'); // 'Hey, Alice'
Rule 4: new Binding
When a function is called with new, this = the newly created object:
function Person(name) {
this.name = name; // this is the new object
}
const alice = new Person('Alice');
console.log(alice.name); // 'Alice'
Rule 5: Arrow Functions (Lexical this)
Arrow functions have no this of their own; they inherit this from the outer lexical scope at their point of definition, and it cannot be changed by call/apply/bind/new:
const obj = {
name: 'Alice',
greet() {
// This is a regular method; this = obj
const arrow = () => {
console.log(this.name); // inherits this from the enclosing greet (= obj)
};
arrow();
arrow.call({ name: 'Bob' }); // still 'Alice'! call has no effect on arrow functions
}
};
obj.greet(); // 'Alice', 'Alice'
Priority of the 5 Rules (Highest to Lowest)
| Priority | Rule | Example |
|---|---|---|
| 1 (highest) | new binding | new Foo() |
| 2 | Explicit binding (bind > call/apply) | foo.call(obj) |
| 3 | Implicit binding | obj.foo() |
| 4 (lowest) | Default binding | foo() |
| Special | Arrow function (lexical binding, outside the above rules) | () => this |
🔸 Level 2 · How It Actually Works
Mapping Between Calling Styles and this Values
┌────────────────────────────────────────────────────────────────────────┐
│ Mapping Between Call Style and this Value │
│ │
│ Call style this value │
│ ────────────────────────────────────────────────────────────────── │
│ foo() → undefined (strict) / globalThis (non) │
│ │
│ obj.foo() → obj (object to the left of . at call site) │
│ obj.a.b.foo() → obj.a.b (closest object to the left of .) │
│ │
│ foo.call(thisArg) → thisArg (null/undefined → global, non-str) │
│ foo.apply(thisArg) → thisArg │
│ foo.bind(thisArg)() → thisArg (new can override this) │
│ │
│ new foo() → newly created object (ignores bind's this) │
│ │
│ arrow function () => ... → this from the outer lexical environment │
│ at the point of definition │
│ (unaffected by any calling convention) │
└────────────────────────────────────────────────────────────────────────┘
Implicit Binding Loss
Implicit binding only takes effect when a function is called through an object. Assigning a method to a variable and then calling it causes implicit binding loss, falling back to default binding:
const obj = {
name: 'Alice',
greet() { console.log(this.name); }
};
obj.greet(); // 'Alice' (implicit binding)
const fn = obj.greet;
fn(); // undefined (default binding, strict) / window.name (non-strict)
// Common case: destructuring assignment
const { greet } = obj;
greet(); // also lost
// Common case: callback
setTimeout(obj.greet, 0); // lost; this is window in non-strict mode
Reason: fn() is a plain call, not a call through an object. The Base of fn's Reference Record is not obj, so the implicit binding rule is not triggered.
How bind Works Internally and Why new Overrides bind
bind returns a new function (a BoundFunctionExoticObject) with [[BoundThis]] storing the bound this, [[BoundTargetFunction]] storing the original function, and [[BoundArguments]] storing preset arguments.
// Simplified simulation of bind's behavior
Function.prototype.myBind = function(thisArg, ...presetArgs) {
const originalFn = this;
return function bound(...callArgs) {
// If called with new, this is the new object; ignore thisArg
if (new.target) {
return new originalFn(...presetArgs, ...callArgs);
}
return originalFn.call(thisArg, ...presetArgs, ...callArgs);
};
};
new overrides bind:
function Person(name) {
this.name = name;
}
const BoundPerson = Person.bind({ name: 'Bound' });
const p = new BoundPerson('Alice');
// new ignores the { name: 'Bound' } bound by bind
// this is the newly created object; name = 'Alice'
console.log(p.name); // 'Alice'
Specification basis: when the BoundFunctionExoticObject's [[Construct]] is invoked, it uses the target function's [[Construct]], passing [[BoundArguments]] + arguments—but not [[BoundThis]]. The new object's this is created internally by [[Construct]] and is unaffected by bind.
Arrow Functions and Lexical this: Not "Inherited"—"Absent"
The FunctionEnvironmentRecord for an arrow function has [[ThisBindingStatus]] set to 'lexical', meaning it has no ThisBinding of its own. When HasThisBinding() is called on an arrow function's ER, it returns false. The ResolveThisBinding algorithm, finding no ThisBinding in the current ER, continues searching outward up the ER chain until it finds an ER that does have a ThisBinding.
ResolveThisBinding path when an arrow function is called:
Arrow function's ER (HasThisBinding = false)
↓ search outward
Outer function's ER (FunctionEnvironmentRecord, HasThisBinding = true)
↓
Return outer function's [[ThisValue]]
This is why call/apply/bind have no effect on arrow functions—not because this is "ignored," but because an arrow function has no ThisBinding at all. ResolveThisBinding does not stop at the arrow function's ER, so there is nothing there to override.
Behavior Differences for call(null) and call(undefined)
function foo() {
console.log(this);
}
// Non-strict mode
foo.call(null); // window (browser) / global (Node.js)
foo.call(undefined); // same
// Strict mode
'use strict';
function bar() {
console.log(this);
}
bar.call(null); // null
bar.call(undefined); // undefined
Specification (OrdinaryCallBindThis algorithm):
- Non-strict mode: if thisArgument is
nullorundefined,this= the global object - Strict mode:
thisis thisArgument as-is, no conversion
This is for backward compatibility: in the ES3 era, call(null) was widely used to mean "I don't care about this." ES5 corrected this behavior in strict mode, but non-strict mode must preserve the original semantics.
🔺 Level 3 · What the Specification Says
Specification Section 9.4.1.3: ResolveThisBinding
ResolveThisBinding() is an abstract operation called when the this expression is evaluated:
ResolveThisBinding():
1. Take the LexicalEnvironment of the current running EC as envRec
2. Assert: envRec must be a FunctionEnvironmentRecord, GlobalEnvironmentRecord,
or ModuleEnvironmentRecord
3. Return envRec.GetThisBinding()
FunctionEnvironmentRecord.GetThisBinding() specification:
FunctionEnvironmentRecord.GetThisBinding():
1. If [[ThisBindingStatus]] is 'lexical' → throw ReferenceError
(In practice this case is never reached directly, because the arrow function's ER
has HasThisBinding() = false; the lookup moves outward before reaching here)
2. If [[ThisBindingStatus]] is 'uninitialized' → throw ReferenceError
(super() has not been called yet in a derived class constructor)
3. Return [[ThisValue]]
More precisely, the spec for evaluating the this keyword (Section 13.2.2):
1. Return ? ResolveThisBinding()
The actual resolution of this in the spec is through walking the running EC's LexicalEnvironment chain: when HasThisBinding returns false (arrow function), the search continues outward until it finds an ER that returns true, then calls GetThisBinding on that ER.
Specification Section 10.4.4: Arrow Function ThisMode
When an arrow function is created via OrdinaryFunctionCreate, the thisMode parameter is 'lexical':
Creating an ArrowFunction:
OrdinaryFunctionCreate(..., thisMode = 'lexical', ...)
→ F.[[ThisMode]] = 'lexical'
When the arrow function is called and PrepareForOrdinaryCall creates a new FunctionEnvironmentRecord:
NewFunctionEnvironment(F, newTarget):
If F.[[ThisMode]] is 'lexical':
env.[[ThisBindingStatus]] = 'lexical'
(No [[ThisValue]] is set, because arrow functions have no this binding)
Subsequent OrdinaryCallBindThis step:
OrdinaryCallBindThis(F, calleeContext, thisArgument):
1. Take F.[[ThisMode]] as thisMode
2. If thisMode is 'lexical' → return (do nothing)
// Arrow functions skip the entire this-binding process!
3. ... (for non-arrow functions, bind this per strict/global mode)
Specification: The Complete Logic of OrdinaryCallBindThis
OrdinaryCallBindThis(F, calleeContext, thisArgument) is the spec operation for binding this during an ordinary function call:
1. Take F.[[ThisMode]] as thisMode
2. If thisMode is 'lexical' → return (arrow function; no binding)
3. Take the LexicalEnvironment of calleeContext as localEnv
4. If thisMode is 'strict':
thisValue = thisArgument
5. Otherwise (non-strict mode):
a. If thisArgument is undefined or null:
thisValue = the global object of the current Realm
b. Else if thisArgument is not an object:
thisValue = ToObject(thisArgument) ← primitive boxing
c. Otherwise:
thisValue = thisArgument
6. Call localEnv.BindThisValue(thisValue)
7. Return thisValue
Step 5b explains a common behavior: in non-strict mode calling foo.call(42), this is Number(42) (a wrapper object), not 42 (the primitive). In strict mode, this is 42.
this in Class Methods: Prototype Method vs. Instance Arrow Function
class Counter {
constructor() {
this.count = 0;
// Approach 1: instance arrow function (defined in constructor)
// Each instance has its own function object; this is lexically bound (always the instance)
this.increment = () => {
this.count++;
};
}
// Approach 2: prototype method (defined on the prototype)
// All instances share one function object; this depends on call style
decrement() {
this.count--;
}
}
const c = new Counter();
const { increment, decrement } = c;
increment(); // OK: arrow function; this is always c
decrement(); // this = undefined (strict mode; implicit binding lost)
Memory impact:
increment(instance arrow function): each instance creates a new function object → 100 instances = 100 function objectsdecrement(prototype method): all instances shareCounter.prototype.decrement→ 1 function object
If a class has a large number of instances, instance arrow functions cause significant memory overhead. The correct approach is to use prototype methods and manually bind only where necessary (typically just in event handlers).
💎 Level 4 · Edge Cases and Traps
Trap 1: Implicit Binding Loss Due to Destructuring
const obj = {
name: 'Alice',
getName() {
return this.name;
}
};
// Direct call: works
console.log(obj.getName()); // 'Alice'
// After destructuring: lost
const { getName } = obj;
console.log(getName()); // undefined (strict) / window.name (non-strict)
// Same problem: passing to a callback
[1, 2, 3].forEach(obj.getName); // lost
// Solution 1: bind
const boundGetName = obj.getName.bind(obj);
console.log(boundGetName()); // 'Alice'
// Solution 2: wrap in an arrow function
console.log([1].map(() => obj.getName())[0]); // 'Alice'
// Solution 3: define methods as arrow functions in the class
class Person {
constructor(name) {
this.name = name;
this.getName = () => this.name; // arrow function; this is always the instance
}
}
Trap 2: setTimeout Causes Implicit Binding Loss
const timer = {
seconds: 0,
start() {
// Wrong: this is lost
setInterval(this.tick, 1000);
},
tick() {
this.seconds++; // this is window (non-strict) or undefined (strict)
console.log(this.seconds);
}
};
timer.start(); // NaN or TypeError
// Fix 1: wrap in an arrow function
const timerFixed1 = {
seconds: 0,
start() {
setInterval(() => this.tick(), 1000); // arrow function; this is timerFixed1
},
tick() {
this.seconds++;
console.log(this.seconds); // 1, 2, 3...
}
};
// Fix 2: bind
const timerFixed2 = {
seconds: 0,
start() {
setInterval(this.tick.bind(this), 1000);
},
tick() {
this.seconds++;
console.log(this.seconds);
}
};
Trap 3: new Overrides bind's this (bind Has No Effect Against new)
function Point(x, y) {
this.x = x;
this.y = y;
}
const PointFromOrigin = Point.bind(null, 0, 0); // preset x=0, y=0
// Verify that new has higher priority than bind
const p = new PointFromOrigin(); // this is the newly created object, not null
console.log(p); // Point { x: 0, y: 0 } (new overrides bind's null this)
// Further verification
function Foo() {
console.log(this === globalThis ? 'global' : 'new object');
console.log(this.constructor === Foo); // true (object created by new)
}
const BoundFoo = Foo.bind({ id: 1 });
BoundFoo(); // plain call: this is { id: 1 } (bind takes effect)
new BoundFoo(); // new call: this is the new object (bind's this is ignored)
Trap 4: Memory Difference Between Instance Arrow Functions and Prototype Methods in Class Methods
class WithArrow {
constructor() {
// Instance arrow function: each instance gets its own function object
this.onClick = (event) => {
console.log(this.id); // this is always the instance
};
this.id = Math.random();
}
}
class WithMethod {
constructor() {
this.id = Math.random();
}
// Prototype method: all instances share one function object
onClick(event) {
console.log(this.id); // this depends on call style
}
}
// Memory comparison (create 10,000 instances)
const arrows = Array.from({ length: 10000 }, () => new WithArrow());
const methods = Array.from({ length: 10000 }, () => new WithMethod());
// arrows: 10,000 onClick function objects (~100 bytes each) = ~1 MB extra overhead
// methods: 1 onClick function object shared by all = virtually no extra overhead
In the era of React class components, this.handleClick = this.handleClick.bind(this) in the constructor was standard practice, but it has the same issue (one bound function per instance). Functional components with Hooks alleviate this with useCallback.
Trap 5: The Legacy Trap of call(null) and call(undefined) in Non-Strict Mode
function getThis() {
return this;
}
// Non-strict mode
console.log(getThis.call(null)); // Window (browser)
console.log(getThis.call(undefined)); // Window (browser)
console.log(getThis.call(0)); // Number {0} (ToObject boxing)
console.log(getThis.call('hello')); // String {'hello'} (ToObject boxing)
// Strict mode
'use strict';
function getThisStrict() {
return this;
}
console.log(getThisStrict.call(null)); // null
console.log(getThisStrict.call(undefined)); // undefined
console.log(getThisStrict.call(0)); // 0 (no boxing; primitive value)
console.log(getThisStrict.call('hello')); // 'hello' (no boxing)
This difference is especially important when using .call(null) to mean "I don't need this":
// Old code: using call(null) to mean "don't care about this"
[1, 2, 3].forEach(function(x) {
console.log(x);
}, null); // second argument is thisArg; passing null → still window in non-strict mode
// Modern equivalent: arrow function; this is irrelevant and unaffected
[1, 2, 3].forEach(x => console.log(x));
The most impactful scenario in practice: legacy codebases are full of patterns like Array.prototype.slice.call(arguments, 0), where the first argument to call is arguments (an object). This behaves consistently in both strict and non-strict mode (arguments object serves as this). But substituting null or undefined would produce different results across modes.
Summary
-
The value of
thisis determined by the calling style, governed by 4 runtime rules in descending priority: new binding (this is the new object), explicit binding (value specified by call/apply/bind), implicit binding (the obj in obj.method()), default binding (undefined in strict mode; the global object in non-strict mode). Arrow functions use lexical binding and do not participate in these 4 rules. -
Implicit binding loss is the most common
thisbug: assigning a method to a variable, destructuring it, or passing it to setTimeout or an event listener all cause implicit binding loss, falling back to default binding. Solutions are wrapping in an arrow function or usingbind. -
newhas higher priority thanbind: innew BoundFn(),[[BoundThis]]is ignored andthisis the newly created object. Preset arguments in[[BoundArguments]]remain effective. -
The nature of arrow functions is not "inheriting the outer this" but "having no ThisBinding of their own": the FunctionEnvironmentRecord's
[[ThisBindingStatus]]is'lexical',HasThisBinding()returns false, andResolveThisBindingsearches outward up the ER chain until it finds an ER with a ThisBinding. WhenOrdinaryCallBindThisis called for an arrow function, it detects[[ThisMode]] === 'lexical'and returns immediately, binding nothing. -
In non-strict mode,
call(null)andcall(undefined)replacethiswith the global object;call(42)wraps42via ToObject intoNumber {42}. In strict mode, whatever is passed is used as-is with no automatic conversion.