Q1After const calc = cart.total;, what is this inside total when you call calc()?
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 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().
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
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.
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
likes = 0— goes onbuttonthisin thelabelinitializer — thebuttonbeing created (same as in constructor)
- Has no
thisof its own this.likes— reads the value onbuttonfrom the enclosing scope- Even when called as
onPress(),thisisbutton
this— whatever is before the dot on the calling linebuttonforbutton.addLikeMethod()undefinedif you pull it out and call it
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.
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
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 Called | Value of this | Result Here |
|---|---|---|
| cart.total(), with cart before the dot | cart, before the dot | Returns 4980 |
| Pulled out, then called as calcTotal() | undefined | Throws a TypeError |
| Return value of cart.total.bind(cart) | cart, passed to bind | Returns 4980 |
| Arrow field pulled out, called as onPress() | button, which owns the field | Like count goes up |
| setTimeout(this.showMessage, 100) | window, in a browser | Thanks for your order, undefined |
| setTimeout(() => this.showMessage(), 100) | The outer method's this | Thanks 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.
Knowledge Check
Answer each question one by one.
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?