Skip to content
elephantoo

`this`, call, apply & bind

Lesson 17 of 34 15 min read

How `this` is decided, why arrow functions differ, and controlling it with call, apply and bind.


this is one of the most confusing parts of JavaScript, mostly because it doesn't work like in Java or Python. The good news: there are only a handful of rules. Learn them, and you'll understand bugs that puzzle many developers — and the call, apply and bind methods that control this.

Run these examples with Node as ES modules (save as .mjs) or in the browser console. Modules use strict mode, which affects one rule below.

Rule 1: method call → this is the object before the dot#

JavaScript
const user = {
  name: "Ada",
  greet() {
    return `Hi, I'm ${this.name}`;
  },
};

console.log(user.greet()); // Hi, I'm Ada

When you call user.greet(), this is user. The same function attached to a different object gets a different this:

JavaScript
function introduce() {
  return `I am ${this.name}`;
}

const ada = { name: "Ada", introduce };
const alan = { name: "Alan", introduce };
console.log(ada.introduce());  // I am Ada
console.log(alan.introduce()); // I am Alan

this is decided at call time, by how the function is called — not where it was defined.

Rule 2: plain function call → this is undefined (strict mode)#

JavaScript
"use strict";
function whoAmI() {
  return this;
}
console.log(whoAmI()); // undefined

In strict mode (all modules and classes), a plain call gives this === undefined. In old non-strict scripts it's the global object (window / globalThis) — which silently hides bugs.

The lost-this bug

JavaScript
"use strict";
const counter = {
  count: 0,
  increment() {
    this.count++;
    return this.count;
  },
};

console.log(counter.increment()); // 1

const inc = counter.increment; // detach the method
try {
  inc(); // plain call → this is undefined
} catch (err) {
  console.log(err.constructor.name, err.message);
}
Output
1
TypeError Cannot read properties of undefined (reading 'count')

This happens whenever you pass a method as a callback: setTimeout(counter.increment, 100), button.addEventListener("click", counter.increment), array.map(obj.method). The function gets called later without its object.

Rule 3: arrow functions don't have their own this#

Arrow functions take this from the scope around them (lexical this), and it can't be changed:

JavaScript
const timer = {
  seconds: 0,
  start() {
    // Arrow callback: `this` is the same as in start(), i.e. `timer`
    const id = setInterval(() => {
      this.seconds++;
      console.log(`${this.seconds}s`);
      if (this.seconds === 3) clearInterval(id);
    }, 100);
  },
};

timer.start();
Output
1s
2s
3s

With a regular function () {} callback, this would be undefined inside it. Before arrows, people wrote const self = this; to work around this.

The flip side: don't use arrows as object methods if you need this:

JavaScript
const shop = {
  name: "Elephantoo Store",
  arrowMethod: () => typeof this, // `this` from the module scope, not shop
  regularMethod() {
    return this.name;
  },
};

console.log(shop.regularMethod()); // Elephantoo Store
console.log(shop.arrowMethod());   // "undefined" in an ES module

Rule 4: new → this is the brand-new object#

JavaScript
function Point(x, y) {
  this.x = x;
  this.y = y;
}
const p = new Point(2, 3);
console.log(p); // Point { x: 2, y: 3 }

Classes work the same way — more in the next lesson.

Rule 5: call, apply and bind set this explicitly#

call — call now, arguments listed

JavaScript
function describe(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const person = { name: "Grace" };
console.log(describe.call(person, "Hello", "!")); // Hello, Grace!

apply — call now, arguments as an array

JavaScript
console.log(describe.apply(person, ["Howdy", "?"])); // Howdy, Grace?

apply was popular for passing arrays as arguments (Math.max.apply(null, nums)). Today, spread does that: describe.call(person, ...args) or Math.max(...nums).

bind — return a new function with this locked in

JavaScript
"use strict";
const counter = {
  count: 0,
  increment() {
    this.count++;
    return this.count;
  },
};

const boundInc = counter.increment.bind(counter);
boundInc();
boundInc();
console.log(counter.count); // 2

setTimeout(boundInc, 10); // safe to pass around now

bind can also pre-fill arguments (partial application):

JavaScript
function multiply(a, b) {
  return a * b;
}
const double = multiply.bind(null, 2); // this = null (unused), a = 2
console.log(double(21)); // 42

A bound function's this can't be changed again — not even by another bind or call.

this in event listeners#

In a DOM listener written as a regular function, this is the element the listener is attached to (same as event.currentTarget):

JavaScript
document.querySelector("#like").addEventListener("click", function () {
  this.classList.toggle("liked"); // `this` is the button
});

document.querySelector("#like").addEventListener("click", (event) => {
  event.currentTarget.classList.toggle("liked"); // arrow: use currentTarget
});

Using event.currentTarget is clearer and works with both styles.

this in classes#

Class methods have the same lost-this problem when passed as callbacks. A common fix is an arrow function as a class field, which captures the instance:

JavaScript
class Clicker {
  count = 0;

  handleClick = () => {
    this.count++;
    return this.count;
  };

  regular() {
    return this?.count;
  }
}

const c = new Clicker();
const { handleClick, regular } = c; // detach both
console.log(handleClick()); // 1 – arrow field keeps `this`
console.log(regular());     // undefined – plain call, this is undefined

The decision checklist#

When you see this, ask in order:

  1. Is it inside an arrow function? → Use this from the enclosing scope.
  2. Was the function called with new? → The new object.
  3. Was it called with call/apply, or is it bound? → The given object.
  4. Was it called as obj.method()? → obj.
  5. Otherwise → undefined (strict) or the global object (sloppy scripts).

Common mistakes#

  • Passing obj.method as a callback and losing this. Use () => obj.method() or obj.method.bind(obj).
  • Writing object methods as arrow functions.
  • Expecting this inside a nested regular function to match the outer method.
  • Using bind inside a render loop or a frequently-called function — it creates a new function every time.

What's next#

Now that this makes sense, you're ready for prototypes, classes and inheritance — how JavaScript shares behaviour between objects.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What decides the value of this inside a regular (non-arrow) function?

  2. 2.What is the difference between call and apply?

  3. 3.const f = user.greet; f(); — why does this.name fail inside greet (in strict mode)?

Finished reading?

Mark this lesson complete to track your progress.