El comportamiento variable de this es de las cosas más confusas que tiene JavaScript. Vamos a analizar las reglas que lo rigen.
Este artículo es un resumen de los capítulos 1 y 2 del libro “this & Object Prototypes” (tercero de la serie “You don’t know JS” de Kyle Simpson), más algunos ejemplos propios. Recomiendo leer el libro y de paso leer toda la serie. No tiene desperdicio. Se puede leer libremente en GitHub.
Las reglas #
Para para saber qué es el this en una función determinada hay que saber dónde y cómo se llamó la función, no dónde se declaró. Esto se conoce como call-site.
Hay 4 reglas para determinar el comportamiento de this a partir del call-site, y un orden de precedencia en caso de que varias reglas sean aplicables en un caso específico.
1 - Default binding #
Es quizá el caso más común. Primero hay que anotar que en JavaScript las variables en el global scope son también propiedades en el objeto global.
var a = 2;
// En el navegador el objeto global es "window"
console.log(window.a); // 2
Cuando una función se llama de manera “normal”, this hace referencia al objeto global.
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
function foo() {
this.a = 2;
}
foo();
console.log(window.a); // 2
Pero hay que tener en cuenta que en strict mode, el this no apunta al objeto global sino que es undefined.
'use strict';
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // TypeError: Cannot read property 'a' of undefined
Otro detalle a tener en cuenta es que en Node.js el default binding no tiene el mismo comportamiento, ya que como lo explican en esta respuesta, en Node.js el código no se ejecuta directamente en el global scope sino que se “envuelve” en un módulo. Por ende:
var a = 2;
// En Node.js el objeto global es "global"
console.log(global.a); // undefined
2 - Implicit binding #
Sucede cuando la función le “pertenece” a un objeto. En este caso el this apunta a dicho objeto.
function foo() {
console.log(this.a);
}
var obj = { a: 2, foo: foo };
obj.foo(); // 2
Como la función se llama como obj.foo(), se dice que obj es el “dueño” de foo, y el this apunta a obj. Solo el último objeto en una cadena de referencias importa para el this:
// En un caso como este, "this" en "foo" apuntaría a "c"
a.b.c.foo();
Da igual si la función se declara dentro o fuera del objeto:
var obj = {
a: 2,
foo: function() {
console.log(this.a);
}
};
obj.foo(); // 2
Pero hay que tener cuidado con algunas trampas en el implicit binding.
function foo() {
console.log(this.a);
}
var obj = { a: 2, foo: foo };
var a = "Oops! Default binding";
var bar = obj.foo;
bar(); // "Oops! Default binding"
En realidad bar es solo una referencia a foo. Además si se mira el call-site se observa que es un llamado plano, bar(). Por ende, se aplica el default binding.
Un ejemplo con callbacks:
function foo() {
console.log(this.a);
}
function doFoo(fn) {
fn();
}
var obj = { a: 2, foo: foo };
var a = "Oops! Default binding";
doFoo(obj.foo); // "Oops! Default binding"
En doFoo, fn es solo una referencia a foo, y el llamado es plano, fn(). Por ende, se aplica el default binding.
3 - Explicit binding #
Las funciones tienen métodos .call() y .apply() que pueden forzar a que una función use un objeto determinado como su this.
function foo() {
console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 2
foo.apply(obj); // 2
Lo que hacen estos métodos es recibir como primer parámetro el objeto que se quiere usar como this, y luego invocar la función con este this. Parámetros adicionales que se envíen a call o apply serán pasados como parámetros a la función.
Con respecto al this, call y apply son idénticas. Tienen diferencias en otros aspectos que no nos interesan en el momento.
También hay un método .bind() que sirve para crear una función a partir de otra, de paso asignándole su this.
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = { a: 2 };
var bar = foo.bind(obj);
var b = bar(3); // 2 3
console.log(b); // 5
var c = bar.call({ a: 4 }, 7); // 2 7
console.log(c); // 9
bar es una función igual a foo, pero cuyo this está asignado a obj. Al usar bind, se hace que el this no se pueda cambiar; esto se conoce como hard binding. Por eso aunque se use call para intentar cambiar el this a otro objeto, este no cambia.
Nota #
Si se pasa null o undefined a un bind, call o apply, estos valores se ignoran y se aplica el default binding. Para evitar problemas, es mejor enviar un objeto vacío cuando no interesa el valor de this.
4 - New #
En JavaScript se puede usar la palabra new antes del llamado a cualquier función. Cuando se hace esto, suceden 4 cosas:
- Se crea un objeto nuevo
- Este objeto es “prototype linked” (para el tema de this no nos interesa)
- Este objeto es el this en la ejecución de la función
- A menos que retorne un objeto diferente explícitamente, la función invocada con new retorna automáticamente este objeto
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
Precedencia de las reglas #
- Si la función se llama con new, this es el nuevo objeto creado.
- Si la función se llama con call, apply o bind, this es el objeto indicado explícitamente.
- Si la función es propiedad de un objeto, this es este objeto.
- Si no se cumple ninguna de las anteriores, se aplica el default binding. Si es strict mode, this es undefined. Si no, es el objeto global.
Arrow functions #
Las arrow functions de ES6 no siguen las reglas mencionadas anteriormente. En su lugar, adoptan el this del scope que las contiene. Esto se conoce como lexical this y no se puede sobrescribir (ni siquiera con new).
function foo() {
setTimeout(function() {
console.log(this.a);
}, 1000);
}
function bar() {
setTimeout(() => {
console.log(this.a);
}, 1000);
}
var a = "Oops! Default binding";
var obj = { a: 2 };
foo.call(obj); // "Oops! Default binding"
bar.call(obj); // 2
Tanto en foo como en bar, el this es obj. Pero en el callback del setTimeout, al usar function se aplica default binding, mientras que al usar una arrow function, esta toma el this del scope que la contiene, es decir, el scope de bar, y por tanto el this es obj.
Otro ejemplo:
function foo() {
return (a) => { console.log(this.a); };
}
var obj1 = { a: 1 };
var obj2 = { a: 2 };
var bar = foo.call(obj1);
bar.call(obj2); // 1
La arrow function que retorna foo tiene el this que tenía foo al momento de ejecutarse, es decir, obj1. Ésta se asigna a bar, y su this no se puede sobrescribir con el bar.call(obj2).