Skip to main content
Advertisement

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):

  1. new binding — new Fn() call
  2. Explicit binding — call(), apply(), bind()
  3. Implicit binding — obj.method() method call
  4. Default binding — standalone function call (global or undefined)
  5. 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
Advertisement