domingo, 30 de septiembre de 2012

Preservando el contexto *this*. $.proxy() y bind()

Como hemos visto en algún post anterior, el valor de this en una función representa el contexto en el que se ejecuta y depende de cómo se la llame. Cuando utilizamos la función como un callback, es muy probable que el valor de this no sea el que esperamos cuando finalmente se ejecuta.
Supongamos que tenemos el siguiente objeto:
var myObject = {
    myName: "Hulk",
    showMyName: function() {  alert(  "My Name Is: " + this.myName );}
}
Es muy importante que, cuando utilicemos showMyName() como callback, this siga haciendo referencia al objeto myObject, para que this.myName sea “Hulk”. Sin embargo, la mayoría de las veces no será así. Al ejecutarse desde otro contexto, el valor de this será diferente. Por ejemplo:

//como callback en setTimeout
setTimeout( myObject.showMyName, 1000); 

//como callback en el click de un botón
$(".button1").click( myObject.showMyName );


En los dos casos perdemos el contexto original y el resultado es:

“My Name Is: undefined”

En el primer caso this será el objeto global window y en el segundo caso será el elemento button del DOM. Ninguno de estos dos objetos tiene una propiedad llamada myName, por lo que this.myName es undefined en ese contexto.

A veces necesitamos asegurarnos de que un callback se va a ejecutar con el contexto que nosotros necesitamos. Es decir, que el valor de this no nos va a dar sorpresas y tendrá el valor que queramos. En cualquier proyecto grande necesitaremos una función que nos asegure esto.

Muchas librerias y frameworks proporcionan una solución propia a este problema, por ejemplo jQuery tiene la función $.proxy(), Prototype tiene una función bind(), Ext JS tiene createDelegate(), Dojo tiene hitch(), etc. La última versión de JavaScript, ECMAscript 5, ha incluido también el método .bind() como solución nativa.

Si tenemos que dar soporte a navegadores que no soportan bind() de ECMAscript 5 y no tenemos necesidad de incluir ninguna de estas librerías o frameworks, podemos crear nosotros mismos nuestra utilidad para esto.

Creando nuestra función proxy

Lo que tenemos que hacer es sencillo, envolver la función original en otra que nos asegure el contexto y que se pueda utilizar como callback en lugar del método original. Es decir, una función intermediaria ( proxy ).

Vamos a crear una función createProxyFunction( context, originalFunc ) con dos argumentos: el contexto que debemos preservar ( this ) y la función original. Devolverá una función que puede usarse exactamente igual que la original, pero asegurando el valor de this. El uso sería tan sencillo como:

var myProxyFunc = createProxyFunction( myObject,  myObject.showMyName ); 
$(".button1").click( myProxyFunc );


o, ahorrandonos la variable intermedia:

$(".button1").click( createProxyFunction( myObject,  myObject.showMyName) );


Podemos hacer esto simplemente utilizando los métodos call() o apply() que tienen todas las funciones en JavaScript. Devolveremos una función que llamará a la original utilizando apply() para asignar el contexto.

Una implementación simple sería:

function createProxyFunction ( context, originalFunc ) {
    var proxyFunction = function() {
        return originalFunc.apply( context, arguments );
};
return proxyFunction;
} 


Podríamos complicarlo un poco para permitir añadir argumentos ‘locales’ que se sumarían a los argumentos con los que se llamará al callback cuando se ejecute:

function createProxyFunction ( context, originalFunc ) {
    //store additional arguments (if any) appart from context and originalFunc 
    var proxyArgs = Array.prototype.slice.call(arguments, 2);
    var proxyFunction = function() {
        var allArgs = proxyArgs.concat ( Array.prototype.slice.call(arguments) );
        return originalFunc.apply( context, allArgs );
};
return proxyFunction;
} 


Utilizamos la función slice() de la ‘clase’ Array porque arguments es un pseudo-array y no la tiene. El valor que devuelve sí es un array real y por eso podemos utilizar concat().

La función que se devuelve en este caso tiene concatenados los argumentos que pasemos a createProxyFunction() (despues de context y originalFunc ) con los que se incluyan en la función original por parte de quien ejecute el callback. Es lo mismo que hace $.proxy() de jQuery.

¿Para qué sirven los argumentos extra?

La opción de añadir parámetros extra es muy útil porque podemos estar manejando información, externa al objeto que enviamos como contexto, que necesitaremos conocer cuando recibamos la llamada a nuestro callback.

Los parámetros que recibimos normalmente en un callback estarán fijados por un API. Por ejemplo, para un evento click, jQuery lanzará nuestro callback con eventObject como único argumento:

eventHandler( eventObject)

Si utilizamos la función $.get() de jQuery para pedir unos datos por AJAX, nuestro callback será invocado cuando los datos estén listos con los argumentos:

myAjaxCallback( data, textStatus, jqXHR )

Puede ocurrir que estemos manejando unos datos concretos y, en función de estos, hagamos una llamada AJAX. La respuesta de la llamada AJAX nos llegará cuando se invoque nuestro callback, pero necesitamos seguir teniendo acceso a los datos que estábamos manejando antes para terminar la tarea. La solución sería añadir estos datos como parámetros extra:

$.get('ajax/test.html', createProxyFunction( myObject, myAjaxCallback, extraParam1, extraParam2 );

Por supuesto la función myAjaxCallback tiene que estar preparada para recibir todos estos parámetros:

myAjaxCallback( extraParam1, extraParam2, data, textStatus, jqXHR ); 


ECMAScript 5 bind()

En la implementación de ECMAScript 5, bind() es un método nativo de las funciones que devuelve una nueva función con el contexto fijado. En nuestro ejemplo lo utilizaríamos así:

$(".button1").click( myObject.showMyName.bind( myObject ) );


También pueden añadirse parámetros extra si los necesitamos:

$(".button1").click( myObject.showMyName.bind( myObject, extraParam1, extraParam2 ) );


Recibiremos los parámetros extra delante:

callbackFunction ( extraParam1, extraParam2, param1, param2 );


En que se diferencia de myObject.showMyName.call( myObject )?

A veces se plantea la pregunta de si podríamos usar directamente:

$(".button1").click( myObject.showMyName.call( myObject ) );


en vez de :

$(".button1").click( myObject.showMyName.bind( myObject ) );


No es lo mismo. La diferencia es que bind() devuelve una función que es la que actúa como callback. Utilizando call() estamos ejecutando directamente el método, no sirve como callback. Las dos expresiones no son equivalentes.


No hay comentarios:

Publicar un comentario