domingo, 23 de septiembre de 2012

JavaScript: El contexto *this* en callbacks

El valor del puntero this en JavaScript es uno de los grandes enigmas con que los que se enfrenta el que empieza a manejar el lenguaje. En principio parece sencillo. Dentro de un objeto, el puntero this hace referencia al propio objeto ¿o no?:
var myObject = {
    message: “Hello!”,
    talk: function() {
        alert(“I say: ” + this.message);
    }
}
cuando hacemos referencia a this.message estamos apuntando a la propiedad message en el contexto del objeto myObject. Por lo tanto this es una referencia al objeto en el que estamos.
Si hacemos:

myObject.talk();

veremos el mensaje:

"I say: Hello"

Parece fácil, pero se complica y mucho.

El contexto de un callback

Supongamos que queremos utilizar el método myObject.talk() como callback o event handler que se ejecuta al pulsar un botón:
$(".button1").click( myObject.talk );
Al hacer click en el botón veremos:

"I say: undefined"

¡¿Undefined?!. Cuando se llama al método myObject.talk al hacer click en el botón, el valor de this.message es undefined. La razón es que this ya no apunta al objeto myObject, hace referencia a otro contexto diferente.

En este caso, al ser un callback de un evento del DOM, el navegador asocia this al elemento que provoca el evento ( si usamos addEventListener,  que es la opción que utiliza jQuery si el navegador lo permite ). Es decir, this es el objeto button. Como dentro de este objeto no hay ninguna propiedad que se llame message, this.message es undefined.

No es el único caso en que se cambia el contexto. Si lo usamos como callback de una llamada AJAX o de setTimeout() tampoco tendrá el valor original.

Y por si las cosas no fueran ya bastante complicadas, resulta que si ponemos el método dentro de una función anónima, el mensaje es correcto:

$(".button1").click(function() {
    myObject.talk();
});


El resultado es:

"I say: Hello!"

Es casi igual que lo que teníamos antes, llamámos al método myObject.talk() pero ahora dentro de una función ¿por qué ahora sí funciona?. Bueno, el valor de this ahora sí es el objeto myObject. Lo explicaremos un poco más adelante.

Vamos a ver como se comporta el puntero this en funciones/métodos y después volveremos sobre este ejemplo para explicar qué es lo que está pasando.

this en funciones y métodos

Cuando tenemos una referencia a this dentro de una función, su valor dependerá siempre de cómo se llama a la función. 

Por ejemplo, si hacemos:
var f1 = myObject.talk;
f1();  //this.message is undefined
Estamos llamando a la función talk() de una forma diferente, desde fuera del objeto. En este caso this será el contexto desde el que ejecutamos f1(), que es el objeto global (window).

 Sin embargo, si ejecutamos la función como un método de un objeto concreto, el valor de this será ese objeto:

myObject.talk(); //this.message is "Hello!"

Podríamos incluso llamar a la misma función desde otro objeto diferente:

var Object2 = {
  message: "I'm OBJECT2",
}

Object2.talk = myObject.talk;

Object2.talk(); //this.message is "I'm OBJECT2"

En el alert en pantalla veremos:

"I say: I'm OBJECT2"

Volviendo al ejemplo del callback

Ahora podemos entender qué estaba pasando en nuestros dos ejemplos iniciales. Recordemos que teníamos la función talk() como callback de dos formas diferentes:
//Example 1
$(".button1").click( myObject.talk );

//Example 2
$(".button1").click(function() {
    myObject.talk();
});


En el primer caso le estamos pasando una referencia directa a nuestra función, para que la ejecute cuando y como quiera. Como hemos visto antes, el valor de this dependerá de cómo se llame a la función. Nosotros ya no tenemos el control sobre cómo se la llamará, por eso el valor del contexto ya no es nuestro objeto.

El navegador, cuando llame al callback, colocará el contexto del objeto del DOM que ha lanzado el evento.

En el segundo caso se llama a la función anónima también con el contexto del elemento button, pero dentro de esta función estamos ejecutando explícitamente un método del objeto myObject:


myObject.talk();

Es una llamada directa al método dentro del objeto concreto y, como hemos visto antes, el contexto será el del objeto que se referencia al llamarlo. Por eso aquí el puntero this es correcto.

Hay varias formas de asegurarnos de que el contexto con el que se llama a una función es el que nosotros queremos, como Function.apply(), Function.call() o utilizando una función proxy, pero esto lo veremos en otro post.

 Fuentes:
  MDN This


No hay comentarios:

Publicar un comentario