4.3 Mastering this
this is one of the most confusing keywords in JavaScript. The core rule is simple: this is determined by how a function is called (arrow functions are the exception).
The 4 Rules of this Binding
Rule 1: Global / Regular Function Call (Default Binding)
Calling a function standalone makes this point to the global object (browser: window, Node.js: globalThis). In strict mode, it is undefined.
function showThis() {
console.log(this);
}
showThis(); // browser: window, Node.js: globalThis
// Strict mode
"use strict";
function showThisStrict() {
console.log(this); // undefined
}
showThisStrict();
// Same rule applies inside nested functions
function outer() {
function inner() {
console.log(this); // window (or undefined in strict mode)
}
inner(); // standalone call
}
outer();
Rule 2: Method Call (Implicit Binding)
When called as a method on an object, this points to that object. The object before the dot (.) is this.
const person = {
name: "Alice",
age: 30,
greet() {
console.log(`Hello! My name is ${this.name}.`);
},
getAge: function() {
return this.age;
},
};
person.greet(); // "Hello! My name is Alice."
console.log(person.getAge()); // 30
// Implicit binding loss — be careful!
const greetFunc = person.greet; // assign method to variable
greetFunc(); // "Hello! My name is undefined." — this becomes global
// Method chaining
const builder = {
parts: [],
add(part) {
this.parts.push(part);
return this; // return this to enable chaining
},
build() {
return this.parts.join(", ");
},
};
console.log(builder.add("A").add("B").add("C").build()); // "A, B, C"
Rule 3: new Constructor Call (new Binding)
Calling a function with new creates a new empty object, and this points to that object.
function Person(name, age) {
// With new, this is the newly created object
this.name = name;
this.age = age;
this.greet = function() {
return `${this.name} (${this.age} years old)`;
};
// this is implicitly returned
}
const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);
console.log(alice.greet()); // "Alice (30 years old)"
console.log(bob.greet()); // "Bob (25 years old)"
console.log(alice instanceof Person); // true
// Calling without new pollutes global scope!
// const carol = Person("Carol", 20); // adds name, age to global (errors in strict mode)
Rule 4: Explicit Binding (call / apply / bind)
Specify this directly with call, apply, or bind.
function introduce(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const alice = { name: "Alice" };
const bob = { name: "Bob" };
// call: arguments separated by commas
console.log(introduce.call(alice, "Hello", "!")); // "Hello, Alice!"
console.log(introduce.call(bob, "Hi", ".")); // "Hi, Bob."
// apply: arguments as an array
console.log(introduce.apply(alice, ["Hi", "~"])); // "Hi, Alice~"
// bind: returns a new function (does not execute immediately)
const bobIntroduce = introduce.bind(bob);
console.log(bobIntroduce("Good day", "!")); // "Good day, Bob!"
// bind with partial application
const aliceGreet = introduce.bind(alice, "Hello");
console.log(aliceGreet("!")); // "Hello, Alice!"
console.log(aliceGreet("?")); // "Hello, Alice?"
this in Arrow Functions
Arrow functions do not have their own this. They capture the this from the enclosing scope at the point of definition.
const obj = {
name: "Object",
values: [1, 2, 3],
// Regular function method
processRegular() {
// this === obj ✓
return this.values.map(function(v) {
// this === window (or undefined in strict mode) ✗
return `${this.name}: ${v}`; // this.name is undefined
});
},
// Arrow function
processArrow() {
// this === obj ✓
return this.values.map((v) => {
// Arrow function: captures this from processArrow (obj) ✓
return `${this.name}: ${v}`;
});
},
};
console.log(obj.processRegular()); // ["undefined: 1", "undefined: 2", "undefined: 3"]
console.log(obj.processArrow()); // ["Object: 1", "Object: 2", "Object: 3"]
Using arrow functions in classes
class EventHandler {
constructor(name) {
this.name = name;
this.count = 0;
}
// Class field arrow function: bound to the instance
handleClick = () => {
this.count++;
console.log(`${this.name} click #${this.count}`);
};
// Regular method: this may be lost when passed as event listener
handleHover() {
console.log(`${this.name} hover`); // this.name may be undefined
}
}
const handler = new EventHandler("Button");
// this is preserved even after extraction
const clickFn = handler.handleClick;
clickFn(); // "Button click #1" — works correctly!
const hoverFn = handler.handleHover;
// hoverFn(); // "undefined hover" — this lost!
Detailed Comparison: call / apply / bind
const user = {
name: "Alice",
points: 100,
};
function addPoints(amount, bonus = 0) {
this.points += amount + bonus;
return `${this.name}: ${this.points} points`;
}
// call: execute immediately, list arguments
console.log(addPoints.call(user, 50, 10)); // "Alice: 160 points"
// apply: execute immediately, arguments as array
console.log(addPoints.apply(user, [30, 5])); // "Alice: 195 points"
// bind: returns new function (execute later)
const addPointsToUser = addPoints.bind(user);
console.log(addPointsToUser(20)); // "Alice: 215 points"
// bind to pre-bind first argument
const addFixedBonus = addPoints.bind(user, 0, 100); // amount=0, bonus=100
console.log(addFixedBonus()); // "Alice: 315 points"
Practical use of apply — spreading an array
const numbers = [5, 3, 8, 1, 9, 2, 7];
// Math.max doesn't accept an array directly
// Math.max(numbers) → NaN
// Method 1: apply
const max1 = Math.max.apply(null, numbers);
// Method 2: spread (modern approach)
const max2 = Math.max(...numbers);
console.log(max1, max2); // 9 9
this Pitfalls in Event Handlers
class Button {
constructor(label) {
this.label = label;
this.clickCount = 0;
}
// Method 1: explicit bind
attachWithBind(element) {
element.addEventListener("click", this.handleClick.bind(this));
}
// Method 2: arrow function wrapper
attachWithArrow(element) {
element.addEventListener("click", () => this.handleClick());
}
// Method 3: class field arrow function
handleClick = () => {
this.clickCount++;
console.log(`${this.label} button clicked ${this.clickCount} time(s)`);
};
}
// DOM-free simulation
const btn = new Button("OK");
const mockElement = {
_handlers: [],
addEventListener(event, handler) {
this._handlers.push(handler);
},
click() {
this._handlers.forEach(h => h());
},
};
btn.attachWithArrow(mockElement);
mockElement.click(); // "OK button clicked 1 time(s)"
mockElement.click(); // "OK button clicked 2 time(s)"
Stable this Patterns in Classes
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.requestCount = 0;
}
// Class field arrow function: always safe
fetchData = async (endpoint) => {
this.requestCount++;
const url = `${this.baseURL}${endpoint}`;
console.log(`[${this.requestCount}] GET ${url}`);
try {
// In real environments: fetch(url)
return { url, count: this.requestCount };
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
};
// Regular method: safe when called directly, be careful when detached
getStats() {
return { baseURL: this.baseURL, requestCount: this.requestCount };
}
}
const client = new ApiClient("https://api.example.com");
// Safe even after destructuring (arrow function field)
const { fetchData } = client;
fetchData("/users"); // [1] GET https://api.example.com/users
// Safe to pass to array methods
["/posts", "/comments"].forEach(client.fetchData);
// [2] GET https://api.example.com/posts
// [3] GET https://api.example.com/comments
Binding Priority Summary
this binding priority (high to low):
newbinding —new Fn()call- Explicit binding —
call(),apply(),bind() - Implicit binding —
obj.method()method call - Default binding — standalone function call (global or undefined)
- Arrow function — ignores all the above rules, uses lexical this
function test() {
console.log(this.x);
}
const obj1 = { x: 1, test };
const obj2 = { x: 2, test };
// new takes precedence over bind
const BoundTest = test.bind({ x: 99 });
const instance = new BoundTest(); // new: this.x is undefined (new object)
// call takes precedence over implicit binding
obj1.test.call(obj2); // 2 (obj2's x)
Pro Tips
Tip 1: Debugging this binding
function debugThis(label) {
return function() {
console.log(`[${label}] this:`, this?.constructor?.name ?? typeof this);
};
}
const obj = {
regular: debugThis("regular"),
arrow: (() => {
const fn = debugThis("arrow");
return fn; // already captured global this
})(),
};
Tip 2: bind cannot be re-bound
function greet() { return this.name; }
const alice = { name: "Alice" };
const bob = { name: "Bob" };
const greetAlice = greet.bind(alice);
const tryBob = greetAlice.bind(bob); // bind only applies once!
console.log(greetAlice()); // "Alice"
console.log(tryBob()); // "Alice" — cannot rebind
console.log(greet.call(bob)); // "Bob" — call always applies
Tip 3: Symbol.hasInstance and instanceof this
class Even {
static [Symbol.hasInstance](value) {
return Number.isInteger(value) && value % 2 === 0;
}
}
console.log(2 instanceof Even); // true
console.log(3 instanceof Even); // false
console.log(10 instanceof Even); // true