Aprende leyendo en orden

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)
El mismo total, distintos valores de this
Cuerpo de total()lee this.itemscart.total()(cart antes del .)calcTotal()(en una variable)total()(desestructurado)this escartthis esundefinedthis esundefinedDevuelve 4980Lanza unTypeErrorLanza unTypeError
Las tres llamadas ejecutan el mismo cuerpo de total. this solo es cart en la llamada que tiene cart a la izquierda del punto.

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().

En el mostrador de una tintorería reutilizas un método que arma una línea con el estado de cada prenda. LaundryItem, shirt y coat ya están declarados.

① Llama al método desde shirt y muestra su línea de estado.

② Asigna ese mismo método de shirt a la propiedad line de coat, llámalo desde coat y muestra el resultado.

③ Saca el mismo método de shirt con desestructuración y llámalo solo, sin objeto.

(Si se ejecuta correctamente, aparecerá una explicación.)

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
Lo que devuelve bind y un subtotal agregado después
cart.total.bind(cart)Función nueva conthis fijado a cartNo es cart.total=== da falseboundTotal()da 4980cart.items.push(500)Llamar otra veza boundTotal()Lee cart.itemsen ese momentoDevuelve 5480
bind no modifica cart.total; devuelve una función nueva. Lo que queda fijado es cart mismo, no el total.

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.

Vas a sacar el método de cobro de una suscripción. Subscription, coffee y tea ya están declarados.

① Guarda en billCoffee el bill de coffee con this fijado a coffee.

② Muestra con billCoffee el cobro de 3 meses.

③ Crea billTea para tea de la misma forma y muestra el cobro de 6 meses.

④ Convierte cada elemento de [1, 12] con billCoffee, únelos con " / " y muestra el resultado.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
De dónde toma addLike su this
Cuerpo de class LikeButton
Inicializadores de campos (en cada new)
  • likes = 0 — se guarda en button
  • this en el inicializador de label — el button que se está creando (igual que en constructor)
Dentro de addLike = () => { ... }
  • No tiene this propio
  • this.likes — lee el valor de button del ámbito que la rodea
  • Aunque la llames como onPress(), this es button
Dentro de addLikeMethod() { ... }
  • this — lo que va a la izquierda del punto en la línea que lo llama
  • button con button.addLikeMethod()
  • undefined si lo sacas y lo llamas
Una función flecha usa el this de los inicializadores de campos. Solo en addLikeMethod cambia this según la línea que lo llama.

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() { ... }.

Vas a armar una tabla de botones de cantidad que relaciona el nombre de cada botón con la función que llama al presionarlo. QuantityStepper, mug, buttons y pressed ya están declarados.

① Define increase, que suma 1 a la cantidad y la devuelve, de forma que funcione aunque la saques.

② Escribe decrease, que resta 1, de la misma forma.

③ Llama a las funciones de buttons en el orden de pressed y muestra la cantidad.

④ Muestra el resultado de buttons.oldPlus() y la cantidad de mug.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado

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
this cuando setTimeout llama a la función
setTimeout(this.showMessage)showMessage sellama tras 100msthis pasaa ser windowGracias por tupedido, undefinedsetTimeout(() =>this.showMessage())Llama a la funciónflecha tras 100msnotifyWithArrow:this es noticeGracias por tupedido, Ana
En las dos filas, setTimeout hace la llamada 100ms después. Dentro de la función flecha, puedes llamar al método con notice a la izquierda del punto.

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 llamaValor de thisResultado aquí
cart.total(), con cart antes del puntocart, antes del puntoDevuelve 4980
Sacado y llamado como calcTotal()undefinedLanza un TypeError
Lo que devuelve cart.total.bind(cart)cart, pasado a bindDevuelve 4980
Campo con flecha llamado como onPress()button, dueño del campoAumentan los «me gusta»
setTimeout(this.showMessage, 100)window, en un navegadorGracias por tu pedido, undefined
setTimeout(() => this.showMessage(), 100)this del método de afueraGracias 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.

En la recepción de un hotel, muestras un aviso de habitación lista 100ms después. RoomGuide, ana, carlos y delay ya están declarados.

① Pasa el announce de ana tal cual a setTimeout.

② Para el announce de carlos, fija this antes de pasarlo.

③ Pasa el announce de ana envuelto en una función flecha.

④ Muestra «Check-in completado» y compara en qué momento aparece respecto a ①–③.

Editor JavaScript / TypeScript

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1Después de const calc = cart.total;, ¿qué es this dentro de total cuando llamas calc()?

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?