learning-front

Nivel 2 · JavaScript: fundamentos del lenguaje

BONUS: this a fondo — quién es this según cómo lo llamas

La regla raíz de this: su valor lo decide el call-site, no dónde se define la función. Todos los casos reales — método, callback, call/apply/bind, arrow léxico, new y evento del DOM — con ejemplos ejecutables y su tabla resumen.

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 this lo 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.

javascript
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:

javascript
// 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:

javascript
// 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.

javascript
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.

javascript
// 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:

javascript
// 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#

javascript
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:

javascript
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:

javascript
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:

javascript
// 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 llamadaQuién es this
obj.metodo()obj (binding implícito)
fn() sueltaundefined (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 normalel elemento del DOM que disparó el evento
event handler: arrowel 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 this sea la instancia (.map, .filter, .forEach, setTimeout dentro de un método de clase).
  • No necesitas que this sea 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 this diná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).
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.