En el capítulo de clases viste que el valor de this puede sorprenderte cuando separas
un método de su instancia. Eso era la superficie. Aquí vamos a fondo.
La regla raíz, de la que todo lo demás es consecuencia:
El valor de
thislo decide el call-site —el punto exacto del código donde se llama la función—, no dónde está definida.
Cada forma de llamar una función tiene su propio mecanismo para decidir qué es this.
Hay seis casos. Los veremos todos, en orden de complejidad, con ejemplos ejecutables.
1. Llamada como método: binding implícito#
La forma más habitual: objeto.metodo(). El objeto a la izquierda del punto en la
llamada es this. Mientras llames el método desde la instancia, no habrá sorpresas.
const heroe = {
nombre: 'Tracer',
victorias: 78,
partidas: 120,
winrate() {
// Se llama como heroe.winrate(): heroe está a la izquierda del punto.
// this es heroe → this.victorias es 78, this.partidas es 120.
return (this.victorias / this.partidas * 100).toFixed(1);
},
};
// → "65.0"
heroe.winrate();
// heroe.winrate() tiene a 'heroe' a la izquierda → this = heroe ✓2. Llamada suelta: binding por defecto (y por qué revienta)#
El problema empieza cuando sacas la función de su objeto y la llamas sin él:
// Guardamos la referencia a la función en una variable.
// fn es la función en sí, sin el vínculo con heroe.
const fn = heroe.winrate;
// Al llamar fn() no hay ningún objeto a la izquierda del punto.
// En módulos JavaScript (los archivos .js que usa Astro, Vite, Node.js…)
// y en modo estricto ('use strict'), this es undefined en este caso.
fn();
// → TypeError: Cannot read properties of undefined (reading 'victorias')
// Dentro de winrate, this.victorias intenta leer 'victorias' de undefined → explota.Este error puede pillar desprevenido porque parece que “es la misma función”. No lo es: la función existe, pero el contexto que la hace funcionar (el objeto con los datos) ya no está.
El escenario más habitual en la práctica es pasar el método como callback:
// forEach llama el callback de forma suelta, sin objeto a la izquierda.
// Pasamos el método como si fuera una función cualquiera.
const metodos = [heroe.winrate];
// m() se llama sin objeto a la izquierda → this es undefined → TypeError.
metodos.forEach((m) => m());
// Una función auxiliar hace lo mismo: recibe la función y la llama suelta.
function ejecutar(fn) {
// fn() no tiene objeto a la izquierda → this es undefined → TypeError
fn();
}
// pasamos el método desconectado de heroe
ejecutar(heroe.winrate);La función no cambia. Lo que cambia es quién la llama y cómo.
3. call y apply: binding explícito con ejecución inmediata#
call y apply te dejan elegir explícitamente qué será this al llamar una función.
La diferencia entre los dos es solo cómo se pasan los argumentos adicionales.
function describir(prefijo, sufijo) {
// this lo decide quien llama a describir via call o apply.
// prefijo y sufijo son argumentos normales de la función.
return prefijo + this.nombre + ' — ' + (this.victorias / this.partidas * 100).toFixed(1) + '%' + sufijo;
}
// call: primer argumento es this; el resto, los argumentos de la función, sueltos.
describir.call(tracer, 'Héroe: ', ' (call)');
// → "Héroe: Tracer — 65.0% (call)"
// Dentro de describir, this es tracer porque call lo pone ahí explícitamente.
// apply: primer argumento es this; el resto, los argumentos en UN ARRAY.
describir.apply(reinhardt, ['Héroe: ', ' (apply)']);
// → "Héroe: Reinhardt — 56.7% (apply)"
// Mismo resultado que call; apply es útil cuando ya tienes los args en un array.call y apply ejecutan la función inmediatamente. Si lo que necesitas es preparar
la función para llamarla más tarde, usas bind.
4. bind: binding explícito con función nueva#
bind no ejecuta la función. Devuelve una función nueva con this fijado de forma
permanente al valor que le des. Esa función nueva siempre usará ese this, sin importar
cómo, cuándo o quién la llame.
// bind(mercy) devuelve una copia de describir con this permanentemente = mercy.
// No la ejecuta: la devuelve lista para llamarla cuando quieras.
const describirMercy = describir.bind(mercy);
// Ahora puedes llamarla suelta, pasarla como callback, lo que sea.
// this siempre será mercy.
describirMercy('Héroe: ', ' (bind)');
// → "Héroe: Mercy — 65.0% (bind)"
// Puedes pasarla como callback a forEach: this sigue siendo mercy en cada llamada.
const prefijos = ['Primero: ', 'Segundo: '];
// forEach la llama suelta (sin objeto a la izquierda), pero bind ya fijó this = mercy
prefijos.forEach(function(prefijo) { console.log(describirMercy(prefijo, '')); });
// → "Primero: Mercy — 65.0%"
// → "Segundo: Mercy — 65.0%"
// this es mercy en todos los casos: bind lo fijó de forma permanente.Diferencia de tres herramientas en una línea:
// ejecuta fn ahora, this = obj, args sueltos
fn.call(obj, a, b)
// ejecuta fn ahora, this = obj, args en array
fn.apply(obj, [a, b])
// devuelve función nueva con this = obj; no ejecuta
fn.bind(obj)5. Arrow functions y this léxico#
Las arrow functions (() => {}) no tienen this propio. En lugar de eso, capturan
el this del scope donde están definidas. No importa cómo ni quién las llame después:
su this es el del momento de definición.
Arrow como método: el caso que no funciona#
const heroeConArrow = {
nombre: 'Genji',
victorias: 72,
partidas: 150,
// Arrow como método del objeto → MAL.
// La arrow se define cuando se crea el objeto literal,
// y su scope exterior en ese momento es el módulo (o la función que lo contiene),
// NO el objeto heroeConArrow.
winrate: () => {
// this NO es heroeConArrow: la arrow heredó el this del módulo → undefined (en modo estricto).
// Como this es undefined, intentar leer this.victorias lanzaría un TypeError.
// Devolvemos 'N/A' como valor de sustitución para que el ejemplo no rompa el playground.
// this es undefined en módulos → condición cumplida, salimos con 'N/A'
if (this === undefined) return 'N/A';
// nunca llega aquí en este contexto
return (this.victorias / this.partidas * 100).toFixed(1);
},
};
// heroeConArrow.winrate() falla: la arrow no tiene acceso a los datos del objeto.Regla: no uses arrow functions directamente como métodos de un objeto o clase.
Usa la sintaxis de función normal (metodo() { ... }), que sí recibe this dinámico.
Arrow dentro de un método: el caso que sí funciona#
El uso práctico real —y el que resuelve el problema de “this perdido en callbacks”— es definir una arrow dentro de un método:
const equipo = {
nombre: 'Los Héroes',
// tracer, reinhardt, mercy del dataset
miembros: [tracer, reinhardt, mercy],
resumen() {
// resumen() se llama como equipo.resumen() → this = equipo (binding implícito).
// La arrow del map se DEFINE aquí, dentro de resumen(), cuyo this es equipo.
// La arrow captura ese this (equipo) y lo conserva en cualquier contexto.
const nombres = this.miembros.map((m) => {
// this sigue siendo equipo aquí, aunque forEach/map lo llame de forma suelta.
// m es cada héroe; this.miembros son los miembros de equipo
return m.nombre;
});
return this.nombre + ': ' + nombres.join(', ');
// → "Los Héroes: Tracer, Reinhardt, Mercy"
},
};
// "Los Héroes: Tracer, Reinhardt, Mercy"
console.log(equipo.resumen());Sin la arrow, map recibiría una función normal, la llamaría suelta, y dentro de ella
this sería undefined. Con la arrow, this es siempre equipo, porque fue capturado
al definir la arrow dentro del método.
Este patrón —arrow dentro de un método de clase— es la solución limpia al problema de
this perdido que viste en clases-y-poo al pasar un método como callback.
6. new: binding de construcción#
Cuando llamas una función con new, el motor crea un objeto vacío, lo pasa como this
al constructor y lo devuelve. Es el mecanismo que estudiaste en el capítulo de clases:
class Heroe {
constructor(nombre, victorias, partidas) {
// new crea un objeto vacío y lo asigna a this.
// guardamos los datos en ese objeto
this.nombre = nombre;
this.victorias = victorias;
this.partidas = partidas;
}
winrate() {
// this es la instancia sobre la que se llama el método.
return (this.victorias / this.partidas * 100).toFixed(1);
}
}
const ana = new Heroe('Ana', 66, 110);
// new crea un objeto nuevo, lo pasa como this al constructor.
// ana es ese objeto ya inicializado.
// → "60.0" (this = ana)
ana.winrate();Este caso ya lo tienes interiorizado de clases-y-poo. Lo incluimos aquí para que la
tabla resumen esté completa.
7. this en un manejador de evento del DOM#
En el DOM, cuando asignas una función normal como event handler, el navegador la
llama con this = el elemento que disparó el evento:
// Ejemplo de página real (no se puede demostrar en consola pura).
// En un playground de preview, funcionaría exactamente así:
const boton = document.getElementById('btn-reinhardt');
// Función normal: this lo pone el navegador = el elemento que recibió el clic.
boton.addEventListener('click', function(evento) {
// this es el botón (boton) porque el navegador lo llama como boton.handler().
// añade/quita la clase CSS
this.classList.toggle('seleccionado');
// → "btn-reinhardt"
console.log('Héroe seleccionado: ' + this.id);
});
// Arrow function: NO tiene this propio. Hereda el scope exterior.
// Si estás en un módulo suelto, this es undefined.
// Si estás dentro de un método de clase, this es la instancia, NO el botón.
boton.addEventListener('click', (evento) => {
// this NO es el botón: es el scope donde se definió la arrow.
// Para acceder al botón, usa evento.target en su lugar.
// correcto con arrow
console.log('Elemento clicado: ' + evento.target.id);
});Resumen: en event handlers, si necesitas trabajar con el elemento que disparó el evento
directamente con this, usa función normal. Si necesitas el this de una clase y el
elemento lo lees de evento.target, arrow funciona bien.
Tabla resumen: quién es this en cada caso#
| Forma de llamada | Quién es this |
|---|---|
obj.metodo() | obj (binding implícito) |
fn() suelta | undefined (módulos / strict) |
fn.call(obj, ...) | obj (binding explícito) |
fn.apply(obj, [...]) | obj (binding explícito) |
fn.bind(obj)() | obj (permanente) |
() => { ... } | el this del scope donde se definió (léxico) |
new Clase() | el objeto recién creado (binding de construcción) |
| event handler: función normal | el elemento del DOM que disparó el evento |
| event handler: arrow | el this del scope exterior (no el elemento) |
Guía práctica: cuándo arrow y cuándo función normal#
Usa arrow cuando:
- Es un callback dentro de un método y necesitas que
thissea la instancia (.map,.filter,.forEach,setTimeoutdentro de un método de clase). - No necesitas que
thissea dinámico: la arrow es más concisa y predecible.
Usa función normal cuando:
- Es un event handler del DOM y necesitas
this= el elemento que disparó el evento. - Es un método de objeto o clase: necesita
thisdinámico para apuntar a cada instancia. - Es un método que una subclase puede sobreescribir o ampliar con
super(herencia): defínelo como método normal, nunca como arrow.
Forward: cuando llegues al nivel 6 (React funcional con hooks), los componentes son
funciones, no clases. useState y useEffect reemplazan el estado de instancia, y los
handlers de JSX son arrows que no necesitan this. La mayor parte de los líos que
acabas de aprender desaparecen en ese contexto. Pero los encontrarás en código legacy,
librerías escritas con clases (algunas utilidades del DOM, plugins de terceros) y Error
Boundaries de React —el único lugar donde React todavía exige una clase.
Pruébalo tú#
Todos los casos en un playground. Edita, cambia el objeto que se pasa a call, convierte
una función normal en arrow y observa cómo cambia this en la consola.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué valor tiene `this` al ejecutar este fragmento? const fn = tracer.winrate; fn();
Tu turno#
El GestorTurnos pasa registrarEntrada como callback a forEach y pierde this.
Predice qué va a pasar, luego arréglalo. Cuando lo tengas, despliega las soluciones
y fíjate en qué separa cada nivel: el salto de OK a Mejor es de patrón anticuado a
herramienta moderna; el de Mejor a Excelente es de saber que funciona a saber cuándo
usar cada herramienta.
Ejercicio · en esta página
Adivina y arregla el this
El GestorTurnos pasa registrarEntrada como callback a forEach y pierde this. Predice qué va a pasar, luego arréglalo. Cuando funcione, despliega las soluciones y fíjate en qué separa cada nivel.
Paso 1: Que funcione
- La consola muestra los tres héroes sin errores.
- Los registros se guardan en gestor.registros.
- El arreglo funciona aunque use un patrón anticuado (self = this).
Paso 2: Que esté bien
- Usas arrow function o bind para conservar this.
- No hay variables auxiliares innecesarias (sin self, sin that).
- Entiendes la diferencia entre arrow y bind y aplicas uno con criterio.
Paso 3: Que sea impecable
- Usas arrow para el callback interno y explicas en un comentario por qué.
- Identificas en qué caso un event handler necesita función normal (this = elemento).
- El código y los comentarios muestran criterio, no solo que funciona.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// SOLUCIÓN OK — Funciona, pero con un patrón anticuado
//
// El problema: pasar this.registrarEntrada a forEach desconecta el método
// de su instancia. Cuando forEach lo llama, ya no hay objeto a la izquierda
// del punto → this es undefined en modo estricto → TypeError.
//
// Solución OK: guardar this en una variable antes del callback.
// Es la técnica que se usaba antes de que existieran las arrow functions
// (anterior a ES6, 2015). Hoy es código legacy, pero aún se ve en proyectos viejos.
// ════════════════════════════════════════════════════════════════════════════
const cola = [
{ nombre: "Tracer", rol: "Daño" },
{ nombre: "Reinhardt", rol: "Tanque" },
{ nombre: "Mercy", rol: "Apoyo" },
];
class GestorTurnos {
constructor() {
// acumula los mensajes de entrada
this.registros = [];
}
registrarEntrada(heroe) {
const mensaje = heroe.nombre + " (" + heroe.rol + ") ha entrado en partida";
// necesita que this sea la instancia
this.registros.push(mensaje);
console.log(mensaje);
}
procesarCola(heroes) {
// guardamos la instancia en una variable del scope exterior
const self = this;
// La función anónima cierra sobre 'self' (un closure).
// Cuando forEach la llama, 'self' sigue siendo la instancia de GestorTurnos.
heroes.forEach(function (heroe) {
// llamamos el método desde la instancia via 'self'
self.registrarEntrada(heroe);
});
}
}
const gestor = new GestorTurnos();
gestor.procesarCola(cola);
// los tres mensajes guardados en el array
console.log(gestor.registros); Por qué este nivel
- Resuelve el problema: la consola muestra los tres héroes y los registros se guardan.
- Pero guarda this en self para pasárselo al callback: patrón de antes de ES6 (anterior a 2015). Hoy es código legacy que cualquier revisor señalaría.
- Una variable extra (self) que existe solo para salvar la limitación de las funciones normales: arrow functions hacen lo mismo en una línea, sin la variable auxiliar.
// ════════════════════════════════════════════════════════════════════════════
// SOLUCIÓN MEJOR — Arrow function o bind: las herramientas modernas
//
// Hay dos enfoques limpios para arreglar la pérdida de this en un callback.
// Aquí se muestran los dos, con sus ventajas, para que elijas con criterio.
// ════════════════════════════════════════════════════════════════════════════
const cola = [
{ nombre: "Tracer", rol: "Daño" },
{ nombre: "Reinhardt", rol: "Tanque" },
{ nombre: "Mercy", rol: "Apoyo" },
];
// ── Versión A: arrow function como callback ───────────────────────────────
// Una arrow no tiene this propio: hereda el del scope donde se DEFINE.
// Se define dentro de procesarCola, cuyo this es la instancia → captura la instancia.
class GestorTurnosArrow {
constructor() {
this.registros = [];
}
registrarEntrada(heroe) {
const mensaje = heroe.nombre + " (" + heroe.rol + ") ha entrado en partida";
this.registros.push(mensaje);
console.log(mensaje);
}
procesarCola(heroes) {
// Arrow function: no tiene this propio → hereda el this de procesarCola.
// procesarCola se llama como gestor.procesarCola(), así que su this es la instancia.
heroes.forEach((heroe) => {
// this es la instancia GestorTurnosArrow
this.registrarEntrada(heroe);
});
}
}
const gestorA = new GestorTurnosArrow();
gestorA.procesarCola(cola);
console.log("Registros (arrow): " + gestorA.registros.length + " entradas");
// ── Versión B: bind para fijar this en el método ─────────────────────────
// bind devuelve una función nueva con this permanentemente fijado al valor dado.
// La función nueva siempre usará ese this, sin importar cómo se la llame.
class GestorTurnosBind {
constructor() {
this.registros = [];
}
registrarEntrada(heroe) {
const mensaje = heroe.nombre + " (" + heroe.rol + ") ha entrado en partida";
this.registros.push(mensaje);
console.log(mensaje);
}
procesarCola(heroes) {
// bind(this) crea una copia de registrarEntrada con this bloqueado a la instancia.
// forEach recibirá esa copia: llámala como quiera, this siempre será la instancia.
heroes.forEach(this.registrarEntrada.bind(this));
}
}
const gestorB = new GestorTurnosBind();
gestorB.procesarCola(cola);
console.log("Registros (bind): " + gestorB.registros.length + " entradas");
// ── ¿Cuándo usar cada uno? ────────────────────────────────────────────────
// Arrow en callback (versión A): más legible cuando el callback es cortito.
// bind (versión B): útil cuando quieres reutilizar el método vinculado en varios sitios.
// Ambas son válidas; la arrow es la más común en código moderno. Por qué es mejor que el anterior
- Muestra los dos enfoques modernos: arrow function inline y bind. Ambos son correctos y limpios.
- La arrow funciona porque hereda el this del método que la contiene. bind funciona porque fija this de forma permanente en una copia del método.
- El código ya explica cuándo usar cada uno: arrow para callbacks cortos inline, bind para reutilizar la referencia vinculada.
- Todavía falta discriminar cuándo NO usar arrow (event handler que necesita el elemento): eso lo añade el nivel Excelente.
// ════════════════════════════════════════════════════════════════════════════
// SOLUCIÓN EXCELENTE — Herramienta correcta según el caso, con criterio
//
// La diferencia entre "mejor" y "excelente" no es qué funciona, sino saber
// CUÁNDO usar arrow y cuándo usar función normal, y explicar el porqué.
//
// Regla:
// - Arrow en callback interno: conserva el this del método que lo contiene.
// - Función normal en event handler: su this dinámico ES el elemento que
// disparó el evento, que es exactamente lo que necesitas.
//
// Los dos casos siguientes lo demuestran con el Team Builder.
// ════════════════════════════════════════════════════════════════════════════
// ── Caso 1: callback interno → arrow ─────────────────────────────────────
// procesarCola necesita que this sea la instancia GestorTurnos dentro del forEach.
// La arrow captura el this del scope donde se DEFINE (dentro de procesarCola).
// procesarCola se invoca como gestor.procesarCola(), así que su this es la instancia.
const cola = [
{ nombre: "Tracer", rol: "Daño" },
{ nombre: "Reinhardt", rol: "Tanque" },
{ nombre: "Mercy", rol: "Apoyo" },
];
class GestorTurnos {
constructor() {
// historial de entradas a partida
this.registros = [];
}
registrarEntrada(heroe) {
const mensaje = heroe.nombre + " (" + heroe.rol + ") ha entrado en partida";
// this es la instancia → correcto con arrow
this.registros.push(mensaje);
console.log(mensaje);
}
procesarCola(heroes) {
// Arrow: hereda this del scope de procesarCola → la instancia GestorTurnos.
// No necesitamos self, bind ni trucos: el comportamiento es el esperado.
heroes.forEach((heroe) => this.registrarEntrada(heroe));
}
}
const gestor = new GestorTurnos();
gestor.procesarCola(cola);
// 3
console.log("Entradas registradas: " + gestor.registros.length);
// ── Caso 2: event handler → función normal ────────────────────────────────
// En el DOM, cuando usas una función normal como event handler,
// el navegador llama la función con this = el elemento que disparó el evento.
// Eso es útil: puedes leer el elemento sin necesidad de parámetros extra.
//
// Con una arrow, this NO sería el botón: sería el scope exterior (la clase,
// el módulo... lo que sea donde se definió). Perderías el acceso directo.
//
// Nota: este snippet es código comentado porque el playground es consola pura
// (sin DOM). En una página real, funcionaría exactamente así.
/*
class PanelHeroes {
constructor(elemento) {
// el div del panel
this.elemento = elemento;
// Función normal: this dinámico = el botón que haga clic.
// Si usáramos arrow aquí, this sería la instancia PanelHeroes, NO el botón.
this.elemento.addEventListener('click', function(evento) {
// this es el elemento que recibió el evento (this.elemento).
// añade/quita la clase CSS 'activo'
this.classList.toggle('activo');
});
}
}
*/
// ── Tabla de decisión ─────────────────────────────────────────────────────
// ¿Necesitas this = instancia? → arrow function
// ¿Necesitas this = elemento del DOM? → función normal
// ¿No necesitas this para nada? → indiferente; arrow es más concisa
// ── Forward: React funcional ──────────────────────────────────────────────
// En React moderno (nivel 6 del curso), los componentes son funciones, no clases.
// Los hooks (useState, useEffect) reemplazan el estado de instancia.
// En ese contexto, la mayor parte de los líos de this desaparecen:
// los callbacks de botones son arrow functions de JSX y this no entra en juego.
// Entender this bien es igual de necesario: lo encontrarás en código legacy
// y en librerías escritas con clases (NestJS, algunas utilidades de DOM...). Por qué es mejor que el anterior
- Elige arrow para el callback interno: hereda el this del método, sin trucos, sin variables extra.
- Explica con código (aunque comentado) el caso opuesto: event handler con función normal, donde this dinámico = el elemento es exactamente lo que se necesita.
- Añade la tabla de decisión y el forward a React: el conocimiento de this contextualizado en el panorama real del curso.
- El criterio es la diferencia: no solo sabe arreglarlo, sabe razonar cuándo arrow, cuándo función normal, cuándo bind.