Learn by reading through in order

Binding this — How the Call Decides this and How to Lock It

How this gets decided when you pull a method into a variable or pass it as a callback: the TypeError you get, locking this with bind, arrow function fields, and passing methods to setTimeout.

Sometimes you pass a method that totals a cart to some other code and call it later. A method that works fine as cart.total() can throw an error once you put it in a variable or pass it to setTimeout, because this changes.

This article covers this binding, which depends on how a function is called, and how to lock this in place with bind or an arrow function.

Pulling a Method Out Breaks It — this in Methods

Say you want to reuse cart.total() for the total shown on the page, so you put it in a variable with const calcTotal = cart.total;. But calling calcTotal() throws an error before it adds anything up, even though the cart hasn't changed.

The value of this inside a method is decided by the line that calls it, not by where the method is written. This is called this binding (this being tied to a value). If you read cart.total without (), you get only the function; cart doesn't come along with it.

When you call calcTotal() with nothing before the dot, this inside a class is undefined.

class Cart {
  constructor(items) {
    this.items = items;   // Subtotal for each item
  }
  total() {
    return this.items.reduce((sum, price) => sum + price, 0);
  }
}
const cart = new Cart([1980, 3000]);

// Call it with cart before the dot
console.log(cart.total());       // 4980

// Put it in a variable / pull it out with destructuring
const calcTotal = cart.total;
const { total } = cart;
console.log(typeof calcTotal);   // function
// calcTotal();                  // TypeError: Cannot read properties of undefined (reading 'items')
// total();                      // TypeError (same reason)
The Same total, Different Values of this
Body of total()reads this.itemscart.total()(cart before dot)calcTotal()(in a variable)total()(destructured)this iscartthis isundefinedthis isundefinedReturns 4980Throws aTypeErrorThrows aTypeError
All three run the same body of total. this is cart only in the call that has cart before the dot.

The middle and right columns both throw TypeError: Cannot read properties of undefined (reading 'items'). Destructuring with const { total } = cart; also copies just the function into a variable, so total() fails for the same reason as calcTotal().

At a dry cleaner's counter, you reuse a method that builds a one-line status for each item. LaundryItem, shirt, and coat are already declared.

① Call shirt's status line as a method and display it.

② Assign the same method from shirt to coat's line property, then call it on coat and display the result.

③ Pull the same method out of shirt with destructuring and call it on its own.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

Keeping this in a Variable — The bind Method

Suppose you want to pass the total calculation as a function to the code that displays the total. As the previous section showed, putting cart.total in a variable leaves cart behind, so you get a TypeError when the receiving code calls it.

Calling bind (a method every function has, which returns a new function with this locked to the value you pass) as cart.total.bind(cart) gives you a function that keeps this as cart, even when you call it with nothing before the dot.

Only the returned function is locked, so use the return value when you call it or pass it along.

class Cart {
  constructor(items) { this.items = items; }   // Subtotal for each item
  total() { return this.items.reduce((sum, price) => sum + price, 0); }
}
const cart = new Cart([1980, 3000]);

// A method you just pulled out gets undefined as this when called
const unbound = cart.total;
// unbound();                             // TypeError

// bind returns a new function with this set to cart
const boundTotal = cart.total.bind(cart);
console.log(boundTotal());                // 4980
console.log(boundTotal === cart.total);   // false

// What's locked is cart itself, so it also reads subtotals added later
cart.items.push(500);
console.log(boundTotal());                // 5480
bind's Return Value and a Subtotal Added Later
cart.total.bind(cart)New function withthis set to cartNot cart.total=== is falseboundTotal()is 4980cart.items.push(500)Call boundTotal()againReads cart's itemsat that momentReturns 5480
bind doesn't change cart.total; it returns a new function. What gets locked is cart itself, not the total.

total takes no arguments, but when you lock a method that does, any arguments you pass to the return value go straight to the original method. Only this is locked, so you can pass different arguments on each call.

You pull out the billing method of a subscription. Subscription, coffee, and tea are already declared.

① Put coffee's bill into billCoffee with this locked to coffee.

② Use billCoffee to display the charge for 3 months.

③ Make billTea for tea the same way and display the charge for 6 months.

④ Convert each element of [1, 12] with billCoffee, join the results with " / ", and display them.

JavaScript / TypeScript Editor

Run code to see output

Writing Methods That Work When Pulled Out — Arrow Functions

Imagine passing the same method to several places on a page as the function to run on a click, such as the like buttons on an article. If you have to add bind every time and forget it just once, that one button throws a TypeError.

An arrow function field (a class field whose initial value is an arrow function) is created separately for each instance every time you call new. Inside a field's initializer, this is the new instance, just like in constructor, and an arrow function, which has no this of its own, uses that this.

So you can pull it out and call it without bind.

class LikeButton {
  likes = 0;
  label = `Likes: ${this.likes}`;   // this in an initializer is the button being created
  // Regular method: this is decided by the calling line
  addLikeMethod() { this.likes += 1; return this.likes; }
  // Arrow function field: uses the same this as the initializer
  addLike = () => { this.likes += 1; return this.likes; };
}
const button = new LikeButton();
console.log(button.label);                     // Likes: 0

// Pull it out and call it without bind
const onPress = button.addLike;
console.log(onPress());                        // 1
console.log(onPress());                        // 2
console.log(Object.keys(button).join(", "));   // likes, label, addLike

// Pulling out a regular method breaks it
const onPressMethod = button.addLikeMethod;
// onPressMethod();                            // TypeError
Where addLike Gets Its this
Body of class LikeButton
Field initializers (run on every new)
  • likes = 0 — goes on button
  • this in the label initializer — the button being created (same as in constructor)
Inside addLike = () => { ... }
  • Has no this of its own
  • this.likes — reads the value on button from the enclosing scope
  • Even when called as onPress(), this is button
Inside addLikeMethod() { ... }
  • this — whatever is before the dot on the calling line
  • button for button.addLikeMethod()
  • undefined if you pull it out and call it
An arrow function uses the this of the field initializers. Only in addLikeMethod does this change with the calling line.

addLike shows up in Object.keys(button) because an arrow function field lives on the instance itself, just like likes; addLikeMethod doesn't show up. That also means creating 100 instances creates 100 addLike functions.

Arrow Functions in Object Literals Don't Get the Object as this

If you write const counter = { add: () => { this.likes += 1 } };, this inside counter.add() isn't counter. An object literal doesn't set this, so the arrow function uses the this from outside. Write it as add() { ... } instead.

You build a table of quantity buttons that maps each button name to the function it calls when pressed. QuantityStepper, mug, buttons, and pressed are already declared.

① Define increase, which adds 1 to the quantity and returns it, so that it works even when pulled out.

② Write decrease, which subtracts 1, the same way.

③ Call the functions in buttons in the order of pressed, and display the quantity.

④ Display the result of buttons.oldPlus() and mug's quantity.

JavaScript / TypeScript Editor

Run code to see output

Passing a Method to Code That Calls It Later — setTimeout

Say you want to display a thank-you message with the customer's name 100ms after an order is placed. If you write setTimeout(this.showMessage, 100) inside a method, the message does appear 100ms later, but the customer's name comes out as undefined.

setTimeout calls the function on its own 100ms later, and in a browser it sets this to window for that call (window is an object the browser provides, one per page). If you wrap it in an arrow function, the this in () => this.showMessage() is the this of the enclosing method, so the method is called on the instance.

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

class OrderNotice {
  constructor(customerName) { this.customerName = customerName; }
  showMessage() { console.log(`Thanks for your order, ${this.customerName}`); }

  // Pass the method as-is. setTimeout is what calls it 100ms later
  notifyDirect() { setTimeout(this.showMessage, 100); }

  // Wrap it in an arrow function / lock it with bind before passing it
  notifyWithArrow() { setTimeout(() => this.showMessage(), 100); }
  notifyWithBind() { setTimeout(this.showMessage.bind(this), 100); }
}

const notice = new OrderNotice("Alice");
notice.notifyDirect();      // Thanks for your order, undefined
notice.notifyWithArrow();   // Thanks for your order, Alice
notice.notifyWithBind();    // Thanks for your order, Alice
await delay(300);           // On this page, wait until all the output has appeared
this When setTimeout Calls the Function
setTimeout(this.showMessage)Calls showMessage100ms laterthis is setto windowThanks for yourorder, undefinedsetTimeout(() =>this.showMessage())100ms later, callsthe arrow functionnotifyWithArrow'sthis is noticeThanks for yourorder, Alice
In both rows, setTimeout makes the call 100ms later. Inside the arrow function, you can call the method with notice before the dot.

The top row doesn't throw a TypeError: unlike with calcTotal(), this isn't undefined but window. Since window has no customerName, undefined is displayed. The table below lists the value of this for each way of calling covered in this article.

How It's CalledValue of thisResult Here
cart.total(), with cart before the dotcart, before the dotReturns 4980
Pulled out, then called as calcTotal()undefinedThrows a TypeError
Return value of cart.total.bind(cart)cart, passed to bindReturns 4980
Arrow field pulled out, called as onPress()button, which owns the fieldLike count goes up
setTimeout(this.showMessage, 100)window, in a browserThanks for your order, undefined
setTimeout(() => this.showMessage(), 100)The outer method's thisThanks for your order, Alice

Wrapping It in a function Doesn't Fix this

If you wrap it as setTimeout(function () { this.showMessage(); }, 100), this inside is window, so it throws a TypeError. The console on this page doesn't show that error at all; you'll only see an Uncaught TypeError in the browser's developer tools. Wrap it in an arrow function instead.

At a hotel front desk, you display a room-ready message 100ms later. RoomGuide, alice, bob, and delay are already declared.

① Pass alice's announce to setTimeout as-is.

② For bob's announce, lock this before passing it.

③ Pass alice's announce wrapped in an arrow function.

④ Display "Check-in complete" and compare when it appears with ①–③.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1After const calc = cart.total;, what is this inside total when you call calc()?

Q2After writing only cart.total.bind(cart);, what happens if you put cart.total in a variable and call it?

Q3When passing a method to setTimeout from inside another method, which way does NOT keep this as the instance?