Chapter 5.3☕ 20 min read

this Binding: All 5 Rules — Complete & Final

Same function, 4 different calls, 4 different this values. But 5 rules solve it all.

01Rule 1: Default Binding — The Fallback

When a function is called standalone — just f() with no object, no new, no bindthis falls back to the default binding.

In sloppy mode (non-strict): this = globalThis (which is window in browsers).

In strict mode: this = undefined.

This is the fallback when no other rule applies. Strict mode exists partly to prevent accidental global pollution via this — if you accidentally write this.x = 5 in sloppy mode, you just created a global variable!

function showThis() {
  console.log(this);
}

// Sloppy mode (non-strict)
showThis(); // window (global object)

// Strict mode
(function() {
  'use strict';
  function strictShowThis() {
    console.log(this);
  }
  strictShowThis(); // undefined
})();
V8 Internals: In V8's bytecode, a standalone function call generates a Call bytecode with undefined as the receiver argument. In sloppy mode, V8's built-in Call implementation coerces an undefined receiver to the global object before entering the function body. In strict mode, it passes undefined directly — no coercion.
02Rule 2: Implicit Binding — The Object Before the Dot

When a function is called as a method on an objectobj.fn()this = obj (the "receiver"). Only the LAST object in the chain matters: a.b.fn()this = b.

CRITICAL: Implicit binding is lost if you assign the method to a variable: const fn = obj.fn; fn();this = default.

Callbacks also lose implicit binding: setTimeout(obj.fn, 1000)this = default (or timer object in some old browsers).

const user = {
  name: 'Sai',
  greet() {
    console.log('Hello, ' + this.name);
  }
};

user.greet(); // 'Hello, Sai' — Implicit binding: this = user

// LOSS OF BINDING — extracting the method
const greetFn = user.greet;
greetFn(); // 'Hello, undefined' — Default binding! this is not user

// LOSS IN CALLBACKS
// setTimeout(user.greet, 1000); // 'Hello, undefined' — lost!

// FIX 1: Arrow wrapper
setTimeout(() => user.greet(), 1000); // 'Hello, Sai' ✅

// FIX 2: .bind()
setTimeout(user.greet.bind(user), 1000); // 'Hello, Sai' ✅
Why is it lost? When you write const greetFn = user.greet, you extract the function value from the object. The variable greetFn holds just the function — no reference to user. When called as greetFn(), there is no object before the dot, so the default binding rule applies instead.
03Rule 3: Explicit Binding — call, apply, bind

Sometimes you need to force what this should be. That is explicit binding.

fn.call(thisArg, arg1, arg2): Calls fn immediately with this = thisArg.

fn.apply(thisArg, [argsArray]): Same as call, but arguments as array.

fn.bind(thisArg): Returns a NEW function permanently bound to thisArg. Can be called later.

bind allows partial application (fixing some arguments). Calling .bind() a second time on an already bound function does NOT change this. Once bound, it is locked.

function introduce(greeting, punctuation) {
  console.log(greeting + ', I am ' + this.name + punctuation);
}

const user1 = { name: 'Sai' };
const user2 = { name: 'Ali' };

// .call() — immediate, individual args
introduce.call(user1, 'Hello', '!'); // 'Hello, I am Sai!'
introduce.call(user2, 'Namaste', '.'); // 'Namaste, I am Ali.'

// .apply() — immediate, args as array
introduce.apply(user1, ['Hey', '??']); // 'Hey, I am Sai??'

// .bind() — returns new function, can be called later
const boundIntroduce = introduce.bind(user1, 'Hi'); // partial application
boundIntroduce('!!!'); // 'Hi, I am Sai!!!'

// Double bind fails to change this
const rebind = boundIntroduce.bind(user2);
rebind(); // 'Hi, I am Sai!!!' — still user1! bind is permanent.
bind creates a "BoundFunctionExoticObject": Calling .bind() a second time on an already bound function does NOT change this. Once bound, it is locked. The spec explicitly prevents rebinding. You can still add more fixed arguments with a second bind, but the this value is permanent.
04Rule 4: new Binding — The Constructor

Using new before a function call creates a brand new object. this inside the constructor = that new object.

The new object is implicitly returned unless the constructor explicitly returns another object.

Priority: new overrides bind and implicit binding.

Arrow functions CANNOT use new (no [[Construct]] slot).

function User(name) {
  // 'this' = brand new empty object created by 'new'
  this.name = name;
  this.isAdmin = false;
  // implicitly returns 'this'
}

const sai = new User('Sai');
console.log(sai.name);    // 'Sai'
console.log(sai.isAdmin); // false

// Constructor returning an object overrides 'this'
function WeirdUser(name) {
  this.name = name;
  return { message: 'I am not this!' }; // explicitly returns different object
}

const weird = new WeirdUser('Ali');
console.log(weird.name);    // undefined — 'this' was discarded!
console.log(weird.message); // 'I am not this!'
Under the hood with new: 1) A new empty object is created. 2) Its [[Prototype]] is set to the constructor's .prototype. 3) this is bound to that new object. 4) The constructor body runs. 5) If the constructor returns a non-object (or nothing), the new object is returned. If it returns an object, that object is returned instead — the new this is discarded.
05Rule 5: Arrow Binding — Lexical this & Priority Order

Arrow functions ignore ALL 4 previous rules. this is lexically bound at definition time — it captures this from its enclosing scope.

Priority order from highest to lowest:

1. new              (overrides everything)
2. Explicit          (call / apply / bind)
3. Implicit          (obj.fn())
4. Default           (standalone call)
5. Arrow             (ignores all, uses enclosing scope)
// PRIORITY DEMO
function Person(name) {
  this.name = name;
}

const obj = { name: 'Wrong' };

// Explicit bind
const boundPerson = Person.bind(obj);
boundPerson('Bound');
console.log(obj.name); // 'Bound' — Explicit won

// new overrides Explicit bind!
const instance = new boundPerson('New Instance');
console.log(instance.name); // 'New Instance' — new won!
console.log(obj.name);      // 'Bound' — obj was NOT modified

// Arrow ignores everything
const arrowPerson = (() => this).bind({ x: 1 });
console.log(arrowPerson()); // global/undefined — bind failed!
📋 Most common interview question: What is the output? Know the priority order: new > explicit > implicit > default. Arrows are a separate category — they never participate in the priority game, they just reach into their enclosing scope.

Lo kar liya — Key Points:

  • ✅ Rule 1: Default binding — standalone call, this = globalThis (sloppy) or undefined (strict)
  • ✅ Rule 2: Implicit binding — obj.fn(), this = obj. LOST if extracted or passed as callback
  • ✅ Rule 3: Explicit binding — call/apply/bind force this. bind creates permanently bound function
  • ✅ Rule 4: new binding — creates new object, this = that object, overrides bind
  • ✅ Rule 5: Arrows — no own this, ignores all 4 rules, uses enclosing scope's this
  • ✅ Priority: new > explicit > implicit > default. Arrows exist outside this hierarchy
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase