JavaScript this: objetos, clases, arrow functions y entrevistas
Copias un método de una clase para usarlo como callback. El método llevaba meses funcionando. Lo pasas como prop a un componente o lo desestructuras, y de pronto llega el clásico: Cannot read properties of undefined. Con setTimeout es peor: no falla, escribe en el objeto equivocado.
No has perdido el objeto. Has perdido el sitio desde el que JavaScript llama a la función.
El problema con JavaScript this no es que sea misterioso. Es que casi todos empezamos con una explicación equivocada: “this es el objeto actual”. Suena bien hasta que extraes un método, usas call, escribes una arrow function o entras en una clase.
En corto: en una función normal, this lo determina la llamada, no el lugar donde escribiste la función. En una arrow function, this se captura del contexto léxico exterior. Si puedes responder “¿quién pone el punto antes de los paréntesis?”, puedes predecir la mayor parte de los casos.
¿Qué es this en JavaScript?
this en JavaScript es el valor que el motor proporciona a una función al ejecutarla; en las funciones normales depende de la forma de invocación. No pertenece permanentemente ni al objeto ni a la definición de la función.
Mira el mismo método llamado de dos maneras:
const usuario = {
nombre: 39;Bezael39;,
saludar() {
return `Hola, ${this.nombre}`;
},
};
usuario.saludar(); // "Hola, Bezael"
const saludar = usuario.saludar;
saludar(); // módulo o "use strict": TypeError · script clásico: "Hola, undefined"
En usuario.saludar(), el receptor de la llamada es usuario; por eso this === usuario. En saludar(), no hay receptor. En código estricto, this es undefined. Los módulos ES y el cuerpo de las clases son siempre estrictos, así que ahí falla en cuanto lees this.nombre. En un script clásico sin "use strict", this pasa a ser el objeto global y obtienes un silencioso "Hola, undefined", que es peor: es el fallo más típico de JavaScript this.
Esta regla importa más que memorizar una lista de excepciones:
| Cómo se ejecuta | Valor de this en una función normal |
Ejemplo |
|---|---|---|
| Método | El objeto a la izquierda del punto | usuario.saludar() |
| Función suelta | undefined en código estricto (módulos, clases); el objeto global en script no estricto |
saludar() |
| Constructor | La nueva instancia | new Usuario() |
call, apply o bind |
El valor indicado explícitamente | saludar.call(usuario) |
| Arrow function | El this del scope donde nació |
() => this.nombre |
Las arrow functions llegaron con ES2015; los campos de clase como valor = 0 o incrementar = () => {} entraron en el estándar con ES2022.
La documentación de MDN sobre this lo resume de forma precisa: para una función normal la vinculación se decide en tiempo de ejecución, según cómo se invoca. Esa es la respuesta que conviene dar antes de tocar ningún bind.
this en objetos: el punto importa, pero solo en esa llamada
Un error frecuente en entrevista es asumir que una función “recuerda” al primer objeto que la contiene. No lo hace:
function describir() {
return this.nombre;
}
const backend = { nombre: 39;API39;, describir };
const frontend = { nombre: 39;UI39;, describir };
backend.describir(); // "API"
frontend.describir(); // "UI"
La función es la misma referencia. Cambia el receptor de cada llamada. Esto explica también por qué const fn = backend.describir; fn() rompe: al extraerla se ha perdido la parte que aportaba el contexto.
No arregles esto convirtiendo cada método en una arrow function. Las arrow functions no son “funciones modernas” que sustituyen a las normales. Son funciones con otra semántica de this.
this en clases métodos de prototipo frente a callbacks
Los métodos declarados en una clase viven en el prototipo y reciben this cuando los invocas como método de una instancia:
class Contador {
valor = 0;
incrementar() {
this.valor += 1;
}
}
const contador = new Contador();
contador.incrementar(); // correcto
setTimeout(contador.incrementar, 100); // pierde la instancia: this.valor acaba en NaN sobre window/Timeout, sin error
setTimeout recibe la función, no el objeto: la ejecuta más tarde, desde la cola del event loop, como función suelta y sin receptor.
Para un callback que debe conservar la instancia tienes dos soluciones deliberadas:
setTimeout(contador.incrementar.bind(contador), 100);
class ContadorConCallback {
valor = 0;
incrementar = () => {
this.valor += 1;
};
}
La segunda suele verse como “auto-bind”, porque la arrow creada como campo de instancia captura el this de esa instancia. Es muy cómoda en callbacks de UI, el mismo problema que el frontend lleva años resolviendo de los callbacks a las signals. En Angular, por ejemplo, saber cuándo un método se pasa como callback (un output, un subscribe, un setTimeout) evita arreglos con bind repartidos por el código; si trabajas con componentes modernos, lo ves aplicado en el curso de Angular Moderno.
Pero tiene coste: el método normal se comparte mediante el prototipo; el campo arrow crea una función nueva por instancia. MDN lo señala explícitamente: esa comodidad puede aumentar el uso de memoria. No suele ser dramático para un componente, pero sí es una razón para no convertir por reflejo cada método de una clase en arrow.
Método de prototipo metodo() {} |
Campo arrow metodo = () => {} |
bind al pasarlo |
|
|---|---|---|---|
| Dónde vive | Una sola función en el prototipo | Una función nueva por instancia | Una función nueva por cada bind |
this al pasarlo como callback |
Se pierde | Se conserva | Se conserva |
La subclase puede llamar a super.metodo() |
Sí | No: es propiedad de instancia, no está en el prototipo | Sí |
Se puede espiar con jest.spyOn(Clase.prototype, 'metodo') |
Sí | No | Sí, si se espía antes del bind |
| Limitación / riesgo | Olvidar el bind rompe en runtime |
Más memoria por instancia; tapa al método homónimo de una subclase y no se puede llamar con super ni mockear vía prototipo |
Lógica dispersa; cada bind crea otra referencia y removeEventListener deja de encontrarla |
La discusión no es nueva ni artificial. En el issue 80 de la propuesta de class fields de TC39, abierto en febrero de 2018, alguien preguntó por qué el ejemplo oficial no usaba arrow fields para enlazar métodos. El hilo sacó dos costes concretos: un método escrito como foo = () => {} no se puede sobrescribir bien desde una subclase ni mockear a través del prototipo. La recomendación que quedó en la conversación: deja la lógica en métodos normales y usa el campo arrow solo como callback fino que llama a ese método.
this en arrow functions: capturan, no aceptan uno nuevo
Una arrow function no crea su propio this. Busca el del contexto exterior cuando se crea y se queda con él:
const pedido = {
id: 39;A-4239;,
imprimirLuego() {
setTimeout(() => {
console.log(this.id); // "A-42"
}, 100);
},
};
Aquí la arrow es perfecta: imprimirLuego se ejecutó como método de pedido, así que la arrow captura ese this.
En cambio, escribir una arrow directamente como método de un literal suele ser un bug:
const pedido = {
id: 39;A-4239;,
imprimir: () => this.id,
};
pedido.imprimir(); // módulo ES: TypeError (this es undefined) · script/CommonJS: undefined
La arrow se creó en el scope exterior al literal, no dentro de una invocación con pedido como receptor. Además, ni call, ni apply, ni bind pueden cambiar el this que ya capturó una arrow. Si una pregunta de entrevista mezcla bind y =>, ese es el detalle que busca.
Preguntas de entrevista sobre this en JavaScript
No memorices resultados. Lee primero la forma de llamada.
class Servicio {
nombre = 39;pagos39;;
normal() { return this.nombre; }
arrow = () => this.nombre;
}
const servicio = new Servicio();
const { normal, arrow } = servicio;
normal(); // TypeError: this es undefined
arrow(); // "pagos"
La pregunta buena no es “¿qué imprime?”. Es: “¿por qué una falla y la otra no?”. normal era un método que necesitaba un receptor en la llamada; arrow cerró sobre el this de la instancia durante su inicialización.
Otra habitual:
const perfil = {
nombre: 39;Ana39;,
mostrar() { return this.nombre; },
};
perfil.mostrar.call({ nombre: 39;Luis39; }); // "Luis"
call gana porque mostrar es normal. Si fuera una arrow, ese call solo pasaría argumentos: no reescribiría el contexto léxico.
Cuándo no usar arrow functions para resolver this
Las arrow functions solucionan callbacks que deben conservar el contexto, no todos los problemas relacionados con objetos.
Primero, no las uses como métodos de objetos que necesitan que el receptor pueda cambiar. Rompes precisamente la vinculación dinámica que hace útil a objeto.metodo().
Segundo, no las uses como constructores: no se pueden llamar con new y no tienen prototype propio. Si estás modelando una entidad instanciable, una arrow no sustituye a una clase ni a una función constructora.
Tercero, no escondas un diseño confuso bajo bind. Si un método se pasa por media aplicación y solo funciona cuando recuerdas vincularlo, quizá el callback debería recibir los datos que necesita como argumentos. Reducir la dependencia de this suele hacer el código más fácil de probar. Los componentes de función de React con hooks van por ese camino: no dependen de this (cómo funcionan los hooks de React por dentro).
Cuarto, no conviertas en campo arrow un método que una subclase deba extender o que tus tests espíen en el prototipo: el campo vive en la instancia y super.metodo() no lo encuentra.
La regla para aplicar hoy
Para predecir JavaScript this antes de ejecutar una función, localiza los paréntesis y pregunta qué hay justo a su izquierda. Si hay un objeto y la función es normal, ese será this. Si no hay nada, será undefined en código estricto (módulos ES y clases), salvo que la API que la invoca, como setTimeout o un listener, le pase su propio this. Si es una arrow, deja de mirar la llamada: mira dónde nació.
Esta es una de esas reglas pequeñas que hace que puedas leer callbacks, componentes y APIs de terceros sin tantear hasta que desaparece el error. Si quieres seguir afinando este tipo de fundamentos con otros developers, en Dominicode Labs tienes la comunidad y las sesiones semanales.
Preguntas frecuentes
¿this siempre es el objeto antes del punto?
No. Solo en una función normal invocada como método, por ejemplo objeto.metodo(). Una arrow function ignora esa llamada y conserva el this de su contexto exterior.
¿Por qué this es undefined al extraer un método de una clase?
Porque al hacer const fn = instancia.metodo, después ejecutas fn() sin receptor. Como las clases son strict mode (y lo mismo en módulos ES), la función recibe undefined como this.
¿Debo usar arrow functions para todos los métodos de clase?
No. Úsalas para métodos que se pasan como callbacks y deben mantener la instancia. Para el resto, prefiere métodos normales: viven en el prototipo y no crean una función por instancia.
¿bind funciona con una arrow function?
Devuelve una función, pero no cambia el this de la arrow. Ese valor ya quedó capturado al crearla; bind solo puede ser útil para preasignar argumentos.
¿Cuál es la respuesta corta sobre this en una entrevista?
En funciones normales, this depende de cómo se llama la función. En arrow functions, es léxico: procede del scope donde se definieron. Después explica el caso concreto desde la invocación, no desde el objeto donde viste escrita la función.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
