JavaScript Advanced #2 this Binding and Call Patterns

6 min read

JavaScript’s this depends on how the function is called. In other languages, this inside a class method always refers to the instance; in JavaScript the call shape decides. This post lays out the rules in one place.

The four rules that decide this #

There’s a priority order — top to bottom.

RuleCall shapethis becomes
1new Func()the newly created instance
2obj.method()obj
3func.call(x) / apply / bindthe specified x
4func()global object (or undefined in strict)

The exception that overrides all four — arrow functions ignore them all and use the this from where they were defined (covered shortly).

Rule 4 — plain call #

plain call
function show() {
  console.log(this);
}

show();   // strict mode: undefined / non-strict: globalThis (window)

The simplest case. In strict mode (modules, classes, let/const from Basics #2 automatically) — undefined. In non-strict — global.

ES Modules and class bodies are auto-strict, so in modern code expect undefined.

Rule 2 — method call #

method call — left of dot is this
const obj = {
  name: 'Curtis',
  greet() {
    console.log(this.name);
  },
};

obj.greet();   // 'Curtis'

The object on the left of the dot becomes this. The most common case.

Detaching loses it #

This is JavaScript’s most common pitfall.

detached → lost
const greet = obj.greet;
greet();   // undefined (or global)

The greet variable holds only the function. Without dot-call, rule 4 applies and this disappears.

This causes accidents when passing methods as callbacks or registering them as event handlers.

event handler — lost
class Counter {
  constructor() {
    this.count = 0;
  }
  increment() {
    this.count++;
    console.log(this.count);
  }
}

const c = new Counter();
button.addEventListener('click', c.increment);
// ✗ on click, this is button (or undefined)

Two fixes.

fix
// 1. wrap with an arrow function
button.addEventListener('click', () => c.increment());

// 2. bind
button.addEventListener('click', c.increment.bind(c));

Rule 3 — call / apply / bind #

Three methods to set this explicitly.

call — call immediately #

call
function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

greet.call({ name: 'Curtis' }, 'hi');
// hi, Curtis

First argument is this, the rest are function arguments. Calls the function immediately.

apply — arguments as an array #

apply
greet.apply({ name: 'Curtis' }, ['hi']);
// hi, Curtis

Same as call but arguments come in an array. Since spread arrived, apply is rarely used directly — fn(...args) is shorter.

bind — return a new function with bound this #

bind
const boundGreet = greet.bind({ name: 'Curtis' });
boundGreet('hi');   // hi, Curtis

boundGreet.call({ name: 'other' }, 'hi');
// hi, Curtis  ← bound this wins

bind doesn’t call — it returns a new function with this bound. Once bound, this doesn’t change no matter how it’s called. Common for callback registration.

bind for partial application of arguments too #

bind partial application
function add(a, b) {
  return a + b;
}

const add5 = add.bind(null, 5);   // this is null, first arg pinned to 5
add5(3);    // 8
add5(10);   // 15

The partial-application pattern from #1 Closures is also possible with bind.

Rule 1 — new call #

new call
function User(name) {
  this.name = name;
}

const u = new User('Curtis');
u.name;   // 'Curtis'

Calling with new — creates a new object, makes it this, and auto-returns it. Class calls boil down to this.

new calls beat all other rules.

Arrow functions — the exception that breaks the rules #

Arrow functions have no this of their own. They take the this from where they’re defined.

this in arrow functions
const obj = {
  name: 'Curtis',
  greetRegular: function() {
    console.log(this.name);
  },
  greetArrow: () => {
    console.log(this.name);   // outer this — usually undefined
  },
};

obj.greetRegular();   // 'Curtis'
obj.greetArrow();     // undefined (sees the this outside the object)

Even though greetArrow is defined inside the object, the arrow’s this captures the surrounding environment (usually module top-level = undefined).

Hence — methods regular, callbacks arrow #

practical guide
class Counter {
  constructor() {
    this.count = 0;
  }
  increment() {                      // method → regular function
    setTimeout(() => {                // callback → arrow
      this.count++;                   // outer this (Counter instance) preserved
    }, 100);
  }
}

React hook callbacks, setTimeout/setInterval, fetch’s .then — all the same pattern.

bind is ignored on arrow functions #

bind doesn't work on arrows
const arrow = () => this;
const bound = arrow.bind({ name: 'Curtis' });
bound();   // still outer this

Arrow functions have no own this — nothing to bind. bind is silently ignored.

Common confusing cases #

1) forEach callback #

this inside forEach
const obj = {
  prefix: '> ',
  items: ['a', 'b', 'c'],
  log() {
    this.items.forEach(function(x) {
      console.log(this.prefix + x);   // ✗ this isn't obj
    });
  },
};

forEach’s callback is a regular function — not a dot call, so it’s a plain call and rule 4 (undefined) applies. Switching to an arrow function fixes it.

fixed with arrow
log() {
  this.items.forEach((x) => {
    console.log(this.prefix + x);   // outer this preserved
  });
}

You can also pass a second argument to forEach (a thisArg).

forEach's thisArg
this.items.forEach(function(x) {
  console.log(this.prefix + x);
}, this);   // second arg is thisArg

After arrow functions arrived, the thisArg pattern is rarely used.

2) DOM event handlers #

event handler — this is currentTarget
button.addEventListener('click', function(e) {
  console.log(this);   // the element the event is on (currentTarget)
});

button.addEventListener('click', (e) => {
  console.log(this);   // outer (usually undefined or window)
});

In old code this was meant to point at the element where the event was attached. Arrow functions break that intent. For the element in DOM handlers, use e.currentTarget — safe regardless of function form.

this and strict mode #

A trap from old JavaScript.

non-strict — this is global
function f() {
  console.log(this);   // window or global
}
f();
strict — undefined
'use strict';
function f() {
  console.log(this);   // undefined
}
f();

ES Modules, class bodies, and ES2015+ code are auto-strict. In modern code, expect this to be undefined on plain calls.

Wrap-up #

What we covered:

  • this is decided by call style — 4 rules
  • Plain call → undefined / method call → left of dot / call,apply,bind → explicit / new → new instance
  • Detaching a method to use as a callback loses this
  • call/apply call immediately; bind returns a bound new function
  • Arrow functions have no own this — captured from outside
  • Methods as regular functions, callbacks as arrows (most common guideline)
  • For a DOM element in a handler, e.currentTarget is safest

In the next post (#3 Prototype Chain) we cover what classes really are — how prototypes and the chain work.

X