13 — this — Four Binding Rules, and Why Arrow Functions Changed Everything

6 min read

"Whatever object we're inside" was my this model, and it failed every time the call site changed. The idea that everything else hangs off: *this is not determined by where a function is defined — it's determined by how the function is called, and there are exactly four binding rules that decide its value.* [1]

The framing that finally landed is the four-rules view. Every function call resolves this through one of four mechanisms, checked in priority order. Once I could name which rule was in play, this stopped being a mystery and started being a lookup:

priority: low → high 1. Default plain call f() global, or undefined (strict) 2. Implicit method call obj.f() this = obj (the "before dot") 3. Explicit forced f.call(obj) this = obj (you set it) 4. New constructor new F() this = fresh empty object Arrow lexical () => {} inherits outer this; ignores all 4 rules

Rule 1 — Default binding

A plain function call — f() — with no object, no call, no new. In sloppy mode, this is the global object (window in browsers). In strict mode, this is undefined [1]. This is the lowest-priority rule; if any other rule applies, it overrides.

Rule 2 — Implicit binding

When a function is called as a property of an object — obj.method() — this inside method is obj, the object "before the dot" [2]. This is the most common and intuitive case:

const user = {
name: "Ave",
greet() { console.log(this.name); }
};
user.greet(); // "Ave" — this is user

The trap is losing implicit binding. If I peel the method off and call it bare, this reverts to default:

const fn = user.greet;
fn(); // undefined (or global) — no object before the dot

That's the classic "callback lost my this" bug — passing user.greet as a callback strips the receiver. The fixes are explicit binding (below) or arrow functions.

Rule 3 — Explicit binding: call, apply, bind

When I want to force this myself, three methods on Function.prototype do it [3][4]:

  • fn.call(thisArg, arg1, arg2) — call fn now, with this set to thisArg, arguments passed individually.
  • fn.apply(thisArg, [args]) — same, but arguments as an array.
  • fn.bind(thisArg, ...preset) — returns a new function with this permanently bound (and optional preset arguments). Doesn't call yet.

call/apply are for immediate invocation; bind is for creating a function with a fixed this to pass around. Function borrowing is the use case for call/apply — invoking one object's method against another object without copying it.

function greet(greeting) { return `${greeting}, ${this.name}`; }
greet.call({ name: "Ave" }, "Hi"); // "Hi, Ave"
const bound = greet.bind({ name: "Ave" }, "Hi");
bound(); // "Hi, Ave"

bind is the durable fix for the lost-this callback problem — setTimeout(user.greet.bind(user), 1000) keeps this as user no matter how the timer calls it.

Rule 4 — new binding

Calling a function with new creates a fresh empty object and sets this to it [1]. This is how constructor functions (pre-class) built instances, and it's the highest-priority rule — new overrides even explicit binding.

function User(name) { this.name = name; }
const u = new User("Ave"); // this = new object, returned automatically

The class syntax does the same thing under the hood — new is still the operator that triggers it.

Arrow functions: the escape hatch

Arrow functions ignore all four rules. They have no this of their own — this inside an arrow is whatever this was in the surrounding lexical scope, fixed at definition [5]. That's why arrows solved the callback problem: instead of remembering to .bind(this), I just write an arrow and this flows in from outside.

function Timer() {
this.seconds = 0;
setInterval(() => this.seconds++, 1000); // arrow inherits this = the Timer instance
}

The trade-off: arrows are wrong for object methods that need their receiver. const obj = { name: "Ave", greet: () => this.name } — this here is not obj, it's whatever the outer scope's this was. For methods, use regular functions or shorthand methods.

How I use this

The diagnostic whenever this is wrong is the same four questions: How was the function called? If it's a plain call, that's default binding (probably the bug). If it's obj.method(), implicit — but check whether the method got detached. If it's call/apply/bind, explicit. If it's new, new binding. And if it's an arrow, none of those — look one scope out. In practice, modern code leans on arrow functions for callbacks (inheriting this is usually what I want) and uses class with regular methods for objects that need a receiver. The four-rules vocabulary is what I reach for only when something's misbehaving — naming the rule pinpoints the fix.

References

[1] Mozilla, "this — JavaScript operator," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

[2] I. Kantor, "Object methods, 'this'," The Modern JavaScript Tutorial, 2024. [Online]. Available: https://javascript.info/object-methods#this-in-methods

[3] Mozilla, "Function.prototype.call()," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call

[4] Mozilla, "Function.prototype.bind()," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

[5] I. Kantor, "Function binding — arrows have no this," The Modern JavaScript Tutorial, 2024. [Online]. Available: https://javascript.info/bind

Knowledge check · Question 1 of 5

What determines the value of `this` inside a regular (non-arrow) function?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!