Saltar al contenido
this.js · devschool

this, bind, call y apply en JavaScript

Lección 12 de 43 · 12 min de lectura · Actualizado el

En esta lección
  1. La regla de oro: importa cómo llamas, no dónde escribes
  2. Los casos uno por uno
  3. El problema: perder el this
  4. Solución 1: envolver en una flecha
  5. Solución 2: bind
  6. Solución 3: métodos con flecha en una clase
  7. call y apply: elegir el this en una llamada
  8. El truco antiguo: const self = this
  9. Cuándo elegir cada solución
  10. Errores frecuentes
  11. Resumen

this es una palabra especial que, dentro de una función, apunta a “el objeto con el que se está trabajando”. Lo has usado ya en objetos y clases, y casi siempre hace lo que esperas. Pero hay situaciones en las que de repente vale undefined y el programa falla con un error que desconcierta. En esta lección verás la regla que decide cuánto vale this en cada caso, por qué se “pierde” al pasar un método como callback y cómo arreglarlo con funciones flecha, bind, call y apply.

Conviene que tengas claros los closures y las funciones flecha antes de seguir.

La regla de oro: importa cómo llamas, no dónde escribes

En casi todos los lenguajes con objetos (Java, PHP, Python), this (o self) es siempre el objeto al que pertenece el método. JavaScript es distinto: el valor de this se decide en el momento de la llamada, según cómo se llame a la función. La misma función puede tener un this distinto cada vez.

Para las funciones normales (las que se escriben con function o con la sintaxis corta de método), la pregunta que debes hacerte es: ¿qué hay a la izquierda del punto cuando se llama?

function presentarse() {
  return `Soy ${this.nombre}`;
}

const ana = { nombre: "Ana", presentarse };
const luis = { nombre: "Luis", presentarse };

console.log(ana.presentarse());  // Soy Ana
console.log(luis.presentarse()); // Soy Luis

Es la misma función guardada en dos objetos. En ana.presentarse(), a la izquierda del punto está ana, así que this es ana. En luis.presentarse(), es luis.

Los casos uno por uno

1. Llamada como método

Es el caso normal y el que usarás el 90 % de las veces:

const bici = {
  modelo: "Orbea Alma",
  kilometros: 1200,
  describir() {
    return `${this.modelo} con ${this.kilometros} km`;
  },
};

console.log(bici.describir()); // Orbea Alma con 1200 km

this es bici porque la llamada es bici.describir().

2. Función suelta

Si llamas a una función sin nada delante del punto, no hay objeto. Qué vale entonces this depende del modo:

  • En modo estricto vale undefined.
  • En modo no estricto (el de los scripts antiguos) vale el objeto global: window en el navegador.
"use strict";

function quienSoy() {
  return this;
}

console.log(quienSoy()); // undefined

El modo estricto es una versión más exigente de JavaScript que convierte despistes en errores. Se activa escribiendo "use strict"; al principio del archivo, pero normalmente no hace falta: los módulos y las clases están siempre en modo estricto. Como hoy casi todo el código moderno usa módulos, quédate con esto: una función suelta tiene this igual a undefined.

Nota: que en modo no estricto this sea window es peor, no mejor. Si una función suelta hace this.total = 0, en vez de dar error crea sin querer una variable global llamada total.

3. Dentro de una clase

En una clase, this dentro del constructor es el objeto nuevo que se está creando con new. Dentro de los métodos, se aplica la regla del punto: es el objeto con el que llamas al método.

class Temporizador {
  constructor(nombre) {
    this.nombre = nombre;   // this = el objeto que crea new
    this.segundos = 0;
  }
  avanzar() {
    this.segundos++;        // this = el objeto de antes del punto
    return `${this.nombre}: ${this.segundos} s`;
  }
}

const plancha = new Temporizador("Plancha");
console.log(plancha.avanzar()); // Plancha: 1 s

Como las clases están en modo estricto, si sacas un método de la clase y lo llamas suelto, this es undefined.

4. Manejadores de eventos

Cuando registras una función normal con addEventListener, el navegador la llama con this igual al elemento que tiene el manejador (el mismo que evento.currentTarget):

const boton = document.querySelector("#guardar");

boton.addEventListener("click", function () {
  this.textContent = "Guardado"; // this = boton
});

Funciona, pero es más claro usar el parámetro del evento, que no depende de cómo esté escrita la función:

boton.addEventListener("click", (evento) => {
  evento.currentTarget.textContent = "Guardado";
});

5. Funciones flecha

Las funciones flecha no tienen this propio. Usan el this del sitio donde se escribieron, igual que usan cualquier otra variable de fuera (es un closure). Por eso son perfectas para callbacks dentro de un método:

const ruta = {
  nombre: "Ruta del Cares",
  etapas: ["Poncebos", "Caín"],
  listar() {
    return this.etapas.map((etapa) => `${this.nombre}: ${etapa}`);
  },
};

console.log(ruta.listar());
// [ 'Ruta del Cares: Poncebos', 'Ruta del Cares: Caín' ]

La flecha de dentro de map usa el this de listar, que es ruta. Si hubieras escrito function (etapa) { ... }, map la llamaría como función suelta y this.nombre fallaría.

Por la misma razón, una flecha no sirve como método: su this sería el de fuera del objeto, no el objeto.

const mal = {
  nombre: "Ejemplo",
  saludar: () => `Hola, ${this.nombre}`, // this NO es mal
};

Tabla de casos

Cómo se llamaQué vale this
objeto.metodo()objeto
funcion() suelta, modo estricto o móduloundefined
funcion() suelta, script no estrictowindow (el objeto global)
new Clase()El objeto nuevo
Manejador con function en addEventListenerEl elemento que escucha
Función flechaEl this de donde se escribió
funcion.call(obj) / apply(obj) / bind(obj)obj

El problema: perder el this

Ahora que conoces la regla, el error más famoso de JavaScript tiene explicación. Mira esto:

const describir = bici.describir;
describir();
// TypeError: Cannot read properties of undefined (reading 'modelo')

Al hacer const describir = bici.describir copias solo la función, no su relación con bici. Cuando la llamas, no hay nada a la izquierda del punto, así que es una función suelta: this es undefined y this.modelo revienta.

Esto casi nunca lo harás a propósito. Lo que sí harás es pasar un método como callback, que es exactamente lo mismo: la otra función recibe la función “pelada” y la llama suelta.

const contador = {
  cuenta: 0,
  sumar() {
    this.cuenta++;
    return this.cuenta;
  },
};

function ejecutar(fn) {
  return fn(); // la llama suelta
}

ejecutar(contador.sumar);
// TypeError: Cannot read properties of undefined (reading 'cuenta')

Los casos reales donde te pasará:

setTimeout(contador.sumar, 1000);                  // se pierde
boton.addEventListener("click", contador.sumar);   // this será el botón
datos.forEach(carrito.anadir);                     // se pierde

El del botón es especialmente traicionero: no da error al registrarlo, pero dentro de sumar el this es el botón, así que this.cuenta++ crea una propiedad cuenta en el botón y tu objeto no cambia.

Solución 1: envolver en una flecha

La forma más sencilla y la más usada: pasa una flecha que llame al método con su punto:

console.log(ejecutar(() => contador.sumar())); // 1

setTimeout(() => contador.sumar(), 1000);
boton.addEventListener("click", () => contador.sumar());

Ahora quien recibe el callback llama a la flecha, y la flecha hace contador.sumar(), con contador a la izquierda del punto. Además, si el método necesita argumentos, se los pasas con naturalidad: () => carrito.anadir(producto, 2).

Solución 2: bind

Todas las funciones tienen el método bind(objeto), que devuelve una función nueva con el this fijado para siempre:

const sumarAtado = contador.sumar.bind(contador);

console.log(ejecutar(sumarAtado)); // 2

Da igual cómo se llame después sumarAtado: su this siempre será contador. Eso tiene una consecuencia: un bind no se puede deshacer ni cambiar con otro bind.

function saludar(saludo, signo) {
  return `${saludo}, ${this.nombre}${signo}`;
}

const saludarAAna = saludar.bind(ana, "Qué tal");
console.log(saludarAAna("?"));          // Qué tal, Ana?
console.log(saludarAAna.bind(luis)("!")); // Qué tal, Ana!  (sigue siendo Ana)

Fíjate en que bind también puede fijar los primeros argumentos. Aquí "Qué tal" queda fijado como saludo, y al llamar solo hay que pasar el signo. Esto se llama aplicación parcial, y sirve aunque la función no use this (en ese caso se pasa null como primer argumento):

function precioConIva(iva, precio) {
  return +(precio * (1 + iva)).toFixed(2);
}

const conIvaGeneral = precioConIva.bind(null, 0.21);
console.log(conIvaGeneral(100)); // 121

bind y removeEventListener

Hay un detalle importante si quitas manejadores: bind crea una función nueva cada vez, así que esto no funciona:

boton.addEventListener("click", contador.sumar.bind(contador));
boton.removeEventListener("click", contador.sumar.bind(contador)); // no quita nada

Son dos funciones distintas. Guarda la función atada en una variable y usa esa misma en los dos sitios.

Solución 3: métodos con flecha en una clase

En las clases puedes declarar un método como propiedad con una flecha. Así cada objeto tiene su propia copia con el this ya fijado, y puedes pasarlo como callback directamente:

class Marcador {
  puntos = 0;

  anotar = () => {
    this.puntos++;
    return this.puntos;
  };
}

const local = new Marcador();
const fn = local.anotar;
console.log(fn()); // 1  (no se pierde el this)

Es cómodo en componentes con muchos manejadores de eventos (lo verás en frameworks como React), pero tiene un coste: cada objeto guarda su propia copia de la función en vez de compartirla. Úsalo solo en los métodos que de verdad vayas a pasar como callback.

call y apply: elegir el this en una llamada

bind devuelve una función para usarla más tarde. Si lo que quieres es llamar ahora mismo con un this concreto, tienes call y apply. Hacen lo mismo y solo cambian en cómo se pasan los argumentos:

console.log(saludar.call(ana, "Hola", "!"));       // Hola, Ana!
console.log(saludar.apply(luis, ["Buenas", "."])); // Buenas, Luis.
MétodoQué haceArgumentos
fn.call(obj, a, b)Llama ya, con this = objUno a uno
fn.apply(obj, [a, b])Llama ya, con this = objEn un array
fn.bind(obj, a)No llama: devuelve una función nuevaFija los primeros

Un truco para recordarlo: apply, array. En código moderno se usan poco. apply se usaba para pasar un array como argumentos sueltos (Math.max.apply(null, notas)), pero hoy eso se escribe con spread: Math.max(...notas). Los verás sobre todo en bibliotecas y en código antiguo.

Las flechas ignoran call, apply y bind

Como una flecha no tiene this propio, no hay nada que cambiar:

const flecha = () => this;
console.log(flecha.call(ana) === ana); // false

El truco antiguo: const self = this

Antes de que existieran las flechas (2015), para usar el this de un método dentro de un callback se guardaba en una variable:

const pedido = {
  productos: ["pan", "leche"],
  imprimir() {
    const self = this; // también verás "that" o "_this"
    this.productos.forEach(function (p) {
      console.log(self.productos.length, p);
    });
  },
};

Funciona gracias a los closures. Si lo ves en código antiguo, ya sabes qué hace; en código nuevo, usa una flecha.

Cuándo elegir cada solución

  • Callback puntual (un setTimeout, un forEach, un evento que no vas a quitar): flecha que envuelve, () => obj.metodo().
  • Manejador que luego tendrás que quitar: guarda en una variable una función atada con bind (o una flecha) y usa esa variable en addEventListener y en removeEventListener.
  • Clase cuyos métodos se pasan mucho como callback: propiedades con flecha.
  • Fijar argumentos de una función: bind con aplicación parcial, o simplemente una flecha.

Errores frecuentes

  • Escribir un método con flecha: { total: 0, sumar: () => this.total++ } no toca el objeto. Usa la sintaxis corta sumar() { ... }.
  • Pasar obj.metodo como callback: a setTimeout, addEventListener, forEach o then. Se pierde el this. Envuélvelo: () => obj.metodo().
  • Pasar obj.metodo() con paréntesis: eso ejecuta el método al momento y pasa su resultado, no la función.
  • Usar function dentro de un método para un callback de map, filter o setTimeout: dentro, this ya no es el objeto. Usa una flecha.
  • Esperar que bind cambie la función original: devuelve una función nueva; la original sigue igual.
  • Quitar un manejador con otro bind: cada bind crea una función distinta; guarda la que registraste.
  • Pensar que this dentro de un manejador es el elemento pulsado: es el elemento que escucha (currentTarget). El pulsado es evento.target, que puede ser un hijo.

Resumen

SituaciónthisConsejo
obj.metodo()objEl caso normal
fn() sueltaundefined (estricto)Es el origen de casi todos los errores
FlechaEl de fueraIdeal para callbacks dentro de métodos
new Clase()El objeto nuevoEn constructor y métodos
addEventListener con functionEl elemento que escuchaMejor evento.currentTarget
fn.bind(obj)obj para siempreDevuelve una función nueva
fn.call(obj, ...) / apply(obj, [...])obj en esa llamadaLlaman al momento
  • El valor de this lo decide la forma de llamar, no el sitio donde se escribió la función (salvo en las flechas).
  • Pasar un método como callback lo separa de su objeto: envuélvelo en una flecha o usa bind.

En la siguiente lección empezarás a trabajar a fondo con texto: cadenas.

Pruébalo tú

Cambia el código y pulsa Ejecutar (o Ctrl + Enter).

this.js
Consola

Pon a prueba lo que has aprendido

[JavaScript] ¿Qué ocurre en este código (modo estricto, como en un módulo)?
const contador = {
  cuenta: 0,
  sumar() { this.cuenta++; return this.cuenta; },
};
function ejecutar(fn) { return fn(); }
console.log(ejecutar(contador.sumar));

[JavaScript] Con la misma función ejecutar, ¿qué imprime esto?
const contador = {
  cuenta: 0,
  sumar() { this.cuenta++; return this.cuenta; },
};
function ejecutar(fn) { return fn(); }
console.log(ejecutar(() => contador.sumar()));

[JavaScript] ¿Qué imprime este código?
function saludar(s) { return `${s}, ${this.nombre}`; }
const ana = { nombre: "Ana" };
const luis = { nombre: "Luis" };
const atada = saludar.bind(ana);
console.log(atada.call(luis, "Hola"));

[JavaScript] ¿Por qué un método escrito como flecha dentro de un objeto literal (saludar: () => this.nombre) no funciona como se espera?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.