Emmanuel Akinfulubi

Lesson 03 of 1515 min read

The Word this

This is the lesson that decides whether OOP ever clicks for you. this is not the object a function lives in. It is whichever object was holding the function at the moment it was called.

By the end: Predict what this is in any call, and fix it when it is wrong.

Almost everyone learns this wrong, in the same way. They learn it as "the object the function belongs to". Then one day a method stops working for no visible reason, and there is nothing in that definition to explain it.

Here is the accurate version.

A bus does not know who is driving it. Whoever sits behind the wheel is "the driver" for as long as they are sitting there. The seat is the function. The person in the seat is this. Leave the seat empty and it belongs to nobody.

Same function, three different owners

function whoAmI() {
  return this.plate;
}

const lagosBus = { plate: 'LAG-441-XA', whoAmI };
const enuguBus = { plate: 'ENU-318-PH', whoAmI };

console.log(lagosBus.whoAmI());
console.log(enuguBus.whoAmI());

// One function. Two results. The function never changed.
console.log(lagosBus.whoAmI === enuguBus.whoAmI);

Output


      

Read the call sites, not the function body. lagosBus.whoAmI() has lagosBus to the left of the dot, so this is lagosBus. That is all there is to it.

The five ways this gets set

Every value this ever takes comes from one of these.

1. Called as a method: this is the object before the dot

const board = {
  label: 'Lagos - Abuja',
  read() { return this.label; },
};

console.log(board.read());

Output


      

2. Called plain: there is no owner

'use strict';

const board = {
  label: 'Lagos - Abuja',
  read() { return this; },
};

// Take the function off the object and call it with no dot.
const loose = board.read;
console.log(loose());

Output


      

In a module or a class body, or any code running in strict mode, an unowned call gives this === undefined. In old sloppy-mode scripts it silently gave you the global object instead, which is how a typo could quietly write to window. Strict mode turning it into undefined is the language doing you a favour.

3. Called with new: this is the brand-new object

function Bus(plate) {
  this.plate = plate;
  console.log('inside the constructor, this is', this);
}

const b = new Bus('ABJ-772-KM');
console.log('and it came back as', b);

Output


      

4. Called with call, apply or bind: you choose

function describe(prefix) {
  return prefix + ' ' + this.plate;
}

const lagosBus = { plate: 'LAG-441-XA' };

// call: arguments listed one by one
console.log(describe.call(lagosBus, 'This is'));

// apply: arguments in an array
console.log(describe.apply(lagosBus, ['Boarding now:']));

// bind: no call yet, just a new function with this permanently fixed
const describeLagosBus = describe.bind(lagosBus);
console.log(describeLagosBus('Fixed to'));

Output


      

5. An arrow function: it has no this of its own

const park = {
  name: 'Jibowu park',

  regular() {
    return this.name;
  },

  arrow: () => {
    // No own `this`, so it uses whatever `this` was where it was written,
    // which out here is not the park at all.
    return this === undefined ? 'this is undefined' : String(this);
  },
};

console.log(park.regular());
console.log(park.arrow());

Output


      

The breakage everyone hits

Now the failure that makes people distrust this. You pass a method somewhere as a callback, and it loses its owner in transit.

The method comes off the object and this goes with it

const counter = {
  label: 'bookings',
  count: 0,
  tick() {
    this.count++;
    return this.label + ': ' + this.count;
  },
};

console.log(counter.tick());

// Hand the method to something else. There is no dot at the eventual call site.
const runIt = counter.tick;
try {
  console.log(runIt());
} catch (err) {
  console.log('Broke:', err.message);
}

// Same failure, dressed up as a callback:
try {
  [1, 2, 3].forEach(counter.tick);
} catch (err) {
  console.log('Broke again:', err.message);
}

Output


      

Nothing is wrong with the method. The call site simply has no object in front of it. Three fixes, in order of how often you should reach for them.

Fix 1: wrap the call in an arrow so the dot survives

const counter = {
  label: 'bookings',
  count: 0,
  tick() { this.count++; return this.count; },
};

[1, 2, 3].forEach(() => counter.tick());
console.log('count:', counter.count);

Output


      

Fix 2: bind, which returns a pre-owned copy of the function

const counter = {
  label: 'bookings',
  count: 0,
  tick() { this.count++; return this.count; },
};

const bound = counter.tick.bind(counter);
[1, 2, 3].forEach(bound);
console.log('count:', counter.count);

Output


      

Fix 3: an arrow stored as a field, so this is captured once

class Counter {
  count = 0;
  // An arrow assigned as a field is created inside the constructor, where
  // `this` is already the new instance. It can never be detached.
  tick = () => {
    this.count++;
    return this.count;
  };
}

const c = new Counter();
const detached = c.tick;
detached();
detached();
console.log('count:', c.count);

Output


      

Where arrows are exactly right

The property that breaks arrows in object literals is the same one that makes them perfect inside methods. An arrow written inside a method inherits that method's this, so callbacks just work.

An arrow inside a method: the borrowing is the feature

const park = {
  name: 'Jibowu park',
  buses: ['LAG-441-XA', 'ABJ-772-KM', 'ENU-318-PH'],

  labelledGood() {
    // Arrow inherits `this` from labelledGood, which is the park.
    return this.buses.map(b => b + ' @ ' + this.name);
  },

  labelledBad() {
    // A regular function callback gets no owner, so this.name explodes.
    return this.buses.map(function (b) {
      return b + ' @ ' + this.name;
    });
  },
};

console.log(park.labelledGood());

try {
  console.log(park.labelledBad());
} catch (err) {
  console.log('labelledBad broke:', err.message);
}

Output


      

A checklist you can actually use

When you are unsure what this is, look at the call, and ask in this order:

  1. Is the function an arrow? Then ignore the call entirely and look at where the arrow was written.
  2. Was it called with new? Then this is the new object.
  3. Was it called with .call, .apply, or is it the result of .bind? Then this is whatever was supplied.
  4. Is there something before the dot at the call site? Then that is this.
  5. None of the above? this is undefined.

Your turn: predict, then check

Four calls below. Write down what you think each logs before you run it. Getting one wrong here is worth more than getting all four right by accident.

Editable. Predict first, then run it.

Output


      

Recap

  • this is set by the call, not by where the function was written.
  • Look immediately to the left of the dot at the call site. That is this.
  • No dot means no owner, and in strict mode that means this is undefined.
  • new sets this to the new object; call, apply and bind let you set it yourself.
  • Arrow functions have no this. They borrow it from where they were written, which makes them wrong as object methods and right as callbacks inside methods.

Saved in this browser only.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.