JavaScript’s this keyword is determined at call-time, not at the point where the function is written. Its value depends entirely on how the function is invoked, not where it is declared. While other languages share a similar keyword, JavaScript’s behavior is unique and often surprising.
Why this exists
Without this, every function that needs to operate on an object must receive that object explicitly:
function greet(person) {
console.log(`Hi, I’m ${person.name}`);
}
const alice = { name: 'Alice' };
const bob = { name: 'Bob' };
greet(alice); // Hi, I’m Alice
greet(bob); // Hi, I’m Bob
Using this lets us remove the extra parameter and rely on the call site to supply the context:
function greet() {
console.log(`Hi, I’m ${this.name}`);
}
greet.call(alice); // Hi, I’m Alice
greet.call(bob); // Hi, I’m Bob
Common misconceptions
- Not self-reference:
thisinside a function does not point to the function the function itself. To keep state on the function object, use a lexical identifier:
function counter() {
counter.count = (counter.count || 0) + 1;
}
- Not lexical scope:
thisnever refers to the function’s lexical scope; its a runtime binding created when the execution context is instantiated.
Call-stack vs. call-site
The call-site is the place in code where a function is actually invoked. Inspecting the call-stack (reachable via a debugger’s stack trace) reveals the true call-site:
function a() { b(); }
function b() { c(); }
function c() {
// breakpoint here: call-stack shows a → b → c
}
a(); // call-site of a is global scope
Four binding rules
1. Default binding
Standalone invocation:
function show() {
console.log(this === window); // true in sloppy mode
}
var x = 42;
show(); // 42 (global leak)
In strict mode the binding becomes undefined:
'use strict';
function show() { console.log(this); }
show(); // undefined
2. Implicit binding
When the call is prefixed with a context object:
const box = {
val: 100,
log() { console.log(this.val); }
};
box.log(); // 100
Property chains resolve to the last object in the chain:
const outer = { inner: box };
outer.inner.log(); // 100
Implicit binding is lost in three common scenarios:
- Alias:
const fn = box.log;
fn(); // undefined (falls back to default)
- Callback parameter:
setTimeout(box.log, 0); // undefined
- Assignment expression:
(box.log = box.log)(); // undefined
3. Explicit binding
Use call, apply, or bind to force a context:
function add(c) { return this.base + c; }
const data = { base: 10 };
console.log(add.call(data, 5)); // 15
console.log(add.apply(data, [7])); // 17
const bound = add.bind(data);
console.log(bound(3)); // 13
Hard-binding pattern:
function hard(fn, ctx) {
return function(...args) {
return fn.apply(ctx, args);
};
}
Built-in helpers such as Array.prototype.forEach accept a thisArg:
[1, 2, 3].forEach(function (n) {
console.log(n + this.offset);
}, { offset: 100 }); // 101 102 103
4. new binding
When a function is invoked with new, four steps occur:
- Create a fresh object.
- Link its prototype.
- Bind
thisto the new object. - Return the object (unless the function explicitly returns an object).
function Point(x, y) {
this.x = x;
this.y = y;
}
const p = new Point(3, 4);
console.log(p.x); // 3
A minimal polyfill for new:
function create(ctor, ...args) {
const obj = Object.create(ctor.prototype);
const res = ctor.apply(obj, args);
return (typeof res === 'object' && res !== null) ? res : obj;
}
Precedence of the rules
newbinding- Explicit binding (
call / apply / bind) - Implicit binding
- Default binding
function demo() { console.log(this.id); }
const a = { id: 'A', demo };
const b = { id: 'B' };
a.demo.call(b); // "B" (explicit > implicit)
new a.demo('ignored'); // {} (new > implicit)
const bound = demo.bind(b);
new bound(); // {} (new > bind)
Binding exceptions
Passing null or undefined
Supplying null/undefined to call/apply/bind reverts to default binding. A safer placeholder is an empty object created with Object.create(null):
const ø = Object.create(null);
fn.apply(ø, [1, 2]);
Indirect references
const obj = { id: 42, log() { console.log(this.id); } };
const copy = obj.log;
copy(); // undefined (default binding)
Soft binding
Soft binding provides a default context while still allowing later overrides:
Function.prototype.softBind = function(ctx) {
const fn = this;
return function(...args) {
return fn.apply(
(!this || this === (window || global)) ? ctx : this,
args
);
};
};
Arrow functions
Arrow functions do not use any of the four rules above. They lexically capture this from their enclosing scope and the binding cannot be changed, even by new:
const timer = {
sec: 1,
start() {
setInterval(() => {
console.log(++this.sec);
}, 1000);
}
};
timer.start(); // 2, 3, 4, …
The arrow function’s this is locked to timer, eliminating the need for .bind(this) or storing const self = this.