Pregunta 1Después de const calc = cart.total;, ¿qué es this dentro de total cuando llamas calc()?
Vinculación de this — cómo la llamada decide this y cómo fijarlo
Cómo se decide this cuando guardas un método en una variable o lo pasas como callback: el TypeError que aparece, fijar this con bind, los campos con función flecha y cómo pasar métodos a setTimeout.
A veces pasas a otra parte del código un método que calcula el total del carrito para llamarlo más tarde. Un método que funciona bien como cart.total() puede fallar en cuanto lo guardas en una variable o se lo pasas a setTimeout, porque this cambia.
Este artículo trata la vinculación de this, que depende de cómo se llama la función, y cómo fijar this con bind o con una función flecha.
Si sacas el método, deja de funcionar — this en los métodos
Supón que quieres reutilizar cart.total() para el total que se muestra en la página, así que lo guardas en una variable con const calcTotal = cart.total;. Pero al llamar calcTotal(), falla antes de sumar nada, aunque el carrito no haya cambiado.
El valor de this dentro de un método no depende de dónde escribiste el método, sino de la línea que lo llama. A esto se le llama vinculación de this (que this quede ligado a un valor). Si lees cart.total sin (), obtienes solo la función; cart no viene con ella.
Cuando llamas calcTotal() sin nada a la izquierda del punto, this dentro de una clase es undefined.
class Cart {
constructor(items) {
this.items = items; // Subtotal de cada producto
}
total() {
return this.items.reduce((sum, price) => sum + price, 0);
}
}
const cart = new Cart([1980, 3000]);
// Llamarlo con cart a la izquierda del punto
console.log(cart.total()); // 4980
// Guardarlo en una variable / sacarlo con desestructuración
const calcTotal = cart.total;
const { total } = cart;
console.log(typeof calcTotal); // function
// calcTotal(); // TypeError: Cannot read properties of undefined (reading 'items')
// total(); // TypeError (por la misma razón)
Las columnas del centro y de la derecha lanzan TypeError: Cannot read properties of undefined (reading 'items'). La desestructuración const { total } = cart; también copia solo la función en una variable, así que total() falla por la misma razón que calcTotal().
Conservar this en una variable — el método bind
Ahora quieres pasar el cálculo del total, como función, al código que muestra el total. Como viste en la sección anterior, si guardas cart.total en una variable, cart se queda atrás, y obtienes un TypeError cuando el código que lo recibe lo llama.
bind es un método que tienen todas las funciones y que devuelve una función nueva con this fijado al valor que le pasas. Si escribes cart.total.bind(cart), obtienes una función en la que this sigue siendo cart, aunque la llames sin nada a la izquierda del punto.
Solo queda fijada la función devuelta, así que usa el valor de retorno cuando la llames o la pases.
class Cart {
constructor(items) { this.items = items; } // Subtotal de cada producto
total() { return this.items.reduce((sum, price) => sum + price, 0); }
}
const cart = new Cart([1980, 3000]);
// Si solo sacas el método, al llamarlo this es undefined
const unbound = cart.total;
// unbound(); // TypeError
// bind devuelve una función nueva con this fijado a cart
const boundTotal = cart.total.bind(cart);
console.log(boundTotal()); // 4980
console.log(boundTotal === cart.total); // false
// Lo que queda fijado es cart mismo, así que también lee los subtotales que agregues después
cart.items.push(500);
console.log(boundTotal()); // 5480
total no recibe argumentos, pero si fijas un método que sí los recibe, los argumentos que pases a la función devuelta llegan tal cual al método original. Solo this queda fijado, así que puedes pasar argumentos distintos en cada llamada.
Métodos que funcionan aunque los saques — funciones flecha
Imagina que pasas el mismo método a varios lugares de la página como la función que se ejecuta al hacer clic, como los botones de «me gusta» de un artículo. Si tienes que agregar bind cada vez y lo olvidas una sola vez, ese botón lanza un TypeError.
Un campo con función flecha (un campo de clase cuyo valor inicial es una función flecha) se crea por separado para cada instancia cada vez que llamas a new. Dentro del inicializador de un campo, this es la instancia nueva, igual que en constructor, y una función flecha, que no tiene this propio, usa ese this.
Por eso puedes sacarla y llamarla sin bind.
class LikeButton {
likes = 0;
label = `Me gusta: ${this.likes}`; // this en un inicializador es el botón que se está creando
// Método normal: this depende de la línea que lo llama
addLikeMethod() { this.likes += 1; return this.likes; }
// Campo con función flecha: usa el mismo this que el inicializador
addLike = () => { this.likes += 1; return this.likes; };
}
const button = new LikeButton();
console.log(button.label); // Me gusta: 0
// Sacarla y llamarla sin bind
const onPress = button.addLike;
console.log(onPress()); // 1
console.log(onPress()); // 2
console.log(Object.keys(button).join(", ")); // likes, label, addLike
// Si sacas un método normal, falla
const onPressMethod = button.addLikeMethod;
// onPressMethod(); // TypeError
likes = 0— se guarda enbuttonthisen el inicializador delabel— elbuttonque se está creando (igual que en constructor)
- No tiene
thispropio this.likes— lee el valor debuttondel ámbito que la rodea- Aunque la llames como
onPress(),thisesbutton
this— lo que va a la izquierda del punto en la línea que lo llamabuttonconbutton.addLikeMethod()undefinedsi lo sacas y lo llamas
addLike aparece en Object.keys(button) porque un campo con función flecha se guarda en la propia instancia, igual que likes; addLikeMethod no aparece. Eso también significa que, si creas 100 instancias, se crean 100 funciones addLike.
En un objeto literal, la función flecha no recibe el objeto como this
Si escribes const counter = { add: () => { this.likes += 1 } };, this dentro de counter.add() no es counter. Un objeto literal no define this, así que la función flecha usa el this de afuera. Escríbelo como add() { ... }.
Pasar un método a código que lo llama más tarde — setTimeout
Cuando se confirma un pedido, quieres mostrar 100ms después un mensaje de agradecimiento con el nombre del cliente. Si escribes setTimeout(this.showMessage, 100) dentro de un método, el mensaje sí aparece 100ms después, pero el nombre del cliente sale como undefined.
setTimeout llama por su cuenta a la función 100ms después y, en un navegador, esa llamada recibe window como this (window es un objeto que proporciona el navegador, uno por página). Si la envuelves en una función flecha, el this de () => this.showMessage() es el this del método que la rodea, así que el método se llama sobre la instancia.
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
class OrderNotice {
constructor(customerName) { this.customerName = customerName; }
showMessage() { console.log(`Gracias por tu pedido, ${this.customerName}`); }
// Pasar el método tal cual. Quien lo llama 100ms después es setTimeout
notifyDirect() { setTimeout(this.showMessage, 100); }
// Envolverlo en una función flecha / fijarlo con bind antes de pasarlo
notifyWithArrow() { setTimeout(() => this.showMessage(), 100); }
notifyWithBind() { setTimeout(this.showMessage.bind(this), 100); }
}
const notice = new OrderNotice("Ana");
notice.notifyDirect(); // Gracias por tu pedido, undefined
notice.notifyWithArrow(); // Gracias por tu pedido, Ana
notice.notifyWithBind(); // Gracias por tu pedido, Ana
await delay(300); // En esta página, espera a que aparezca toda la salida
La fila de arriba no lanza un TypeError: a diferencia de calcTotal(), this no es undefined, sino window. Como window no tiene customerName, se muestra undefined. La tabla de abajo resume el valor de this en cada forma de llamar que vimos en este artículo.
| Cómo se llama | Valor de this | Resultado aquí |
|---|---|---|
| cart.total(), con cart antes del punto | cart, antes del punto | Devuelve 4980 |
| Sacado y llamado como calcTotal() | undefined | Lanza un TypeError |
| Lo que devuelve cart.total.bind(cart) | cart, pasado a bind | Devuelve 4980 |
| Campo con flecha llamado como onPress() | button, dueño del campo | Aumentan los «me gusta» |
| setTimeout(this.showMessage, 100) | window, en un navegador | Gracias por tu pedido, undefined |
| setTimeout(() => this.showMessage(), 100) | this del método de afuera | Gracias por tu pedido, Ana |
Envolverlo en function no arregla this
Si lo envuelves como setTimeout(function () { this.showMessage(); }, 100), el this de adentro es window, así que lanza un TypeError. La consola de esta página no muestra ese error; solo verás un Uncaught TypeError en las herramientas para desarrolladores del navegador. Envuélvelo en una función flecha.
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Si solo escribes cart.total.bind(cart); y luego guardas cart.total en una variable y la llamas, ¿qué pasa?
Pregunta 3Al pasar un método a setTimeout desde dentro de otro método, ¿qué forma NO conserva this como la instancia?