2010-01-08 14 views
83

Estoy empezando a usar JavaScript prototypal y tengo problemas para encontrar la forma de conservar una referencia this al objeto principal desde el interior de una función de prototipo cuando cambia el alcance . Permítanme ilustrar lo que quiero decir (estoy usando jQuery aquí):Preservar una referencia a "esto" en las funciones de prototipo de JavaScript

MyClass = function() { 
    this.element = $('#element'); 
    this.myValue = 'something'; 

    // some more code 
} 

MyClass.prototype.myfunc = function() { 
    // at this point, "this" refers to the instance of MyClass 

    this.element.click(function() { 
    // at this point, "this" refers to the DOM element 
    // but what if I want to access the original "this.myValue"? 
    }); 
} 

new MyClass(); 

Sé que puedo conservar una referencia al objeto principal al hacer esto al comienzo de myfunc:

var myThis = this; 

y luego usando myThis.myValue para acceder a la propiedad del objeto principal. ¿Pero qué sucede cuando tengo un montón de funciones de prototipo en MyClass? ¿Debo guardar la referencia al this al principio de cada uno? Parece que debería haber una manera más limpia. Y qué decir de una situación como esta:

MyClass = function() { 
    this.elements $('.elements'); 
    this.myValue = 'something'; 

    this.elements.each(this.doSomething); 
} 

MyClass.prototype.doSomething = function() { 
    // operate on the element 
} 

new MyClass(); 

En ese caso, no puede crear una referencia al objeto principal con var myThis = this; porque incluso el valor original de this en el contexto de doSomething es un objeto, y no jQuery un objeto MyClass.

Se me ha sugerido que use una variable global para mantener la referencia al this original, pero eso me parece una idea realmente mala. No quiero contaminar el espacio de nombres global y parece que me impediría crear dos objetos diferentes MyClass sin que interfieran entre sí.

¿Alguna sugerencia? ¿Hay una manera limpia de hacer lo que estoy buscando? ¿O es mi patrón de diseño completo defectuoso?

Respuesta

60

Para preservar el contexto, el método bind es realmente útil, es ahora parte de la recientemente publicada ECMAScript 5th Edition especificación, la implementación de esta función es simple (sólo 8 líneas de largo):

// The .bind method from Prototype.js 
if (!Function.prototype.bind) { // check if native implementation available 
    Function.prototype.bind = function(){ 
    var fn = this, args = Array.prototype.slice.call(arguments), 
     object = args.shift(); 
    return function(){ 
     return fn.apply(object, 
     args.concat(Array.prototype.slice.call(arguments))); 
    }; 
    }; 
} 

Y se podría utilizar, en su ejemplo como este:

MyClass.prototype.myfunc = function() { 

    this.element.click((function() { 
    // ... 
    }).bind(this)); 
}; 

Otro ejemplo:

var obj = { 
    test: 'obj test', 
    fx: function() { 
    alert(this.test + '\n' + Array.prototype.slice.call(arguments).join()); 
    } 
}; 

var test = "Global test"; 
var fx1 = obj.fx; 
var fx2 = obj.fx.bind(obj, 1, 2, 3); 

fx1(1,2); 
fx2(4, 5); 

En este segundo ejemplo podemos observar más sobre el comportamiento de bind.

Básicamente genera una nueva función, que será la responsable de llamar a nuestra función, conservando el contexto de la función (valor this), que se define como el primer argumento de bind.

El resto de los argumentos se pasan simplemente a nuestra función.

Nota en este ejemplo que la función fx1, se invoca sin ningún contexto objeto (obj.method()), al igual que una simple llamada de función, en este tipo de invocación, la palabra clave this interior se referirá al objeto global, alertará a "prueba global".

Ahora, la fx2 es la nueva función que generó el método bind, llamará a nuestra función preservando el contexto y pasando correctamente los argumentos, alertará "obj test 1, 2, 3, 4, 5" porque invocado agregando los dos argumentos adicionales, ya tenía encerrado los tres primeros.

+0

Me gusta mucho esta funcionalidad pero en un entorno jQuery me inclinaría a nombrarlo de otra forma dado el método jQuery.bind existente (aunque no existe un conflicto de nombres real). –

+0

@Rob, es trivial hacer un método 'jQuery.bind' que se comporte de la misma manera que' Function.prototype.bind', verifique esta implementación simple: http://jsbin.com/aqavo/edit aunque lo consideraría cambiar su nombre, podría causar confusión con el método Events/bind ... – CMS

+2

Recomiendo encarecidamente seguir con el nombre 'Function.prototype.bind'. Ahora es una parte estandarizada del lenguaje; no va a desaparecer – bobince

1

Dado que está utilizando jQuery, vale la pena señalar que this ya se mantiene por sí mismo jQuery:

$("li").each(function(j,o){ 
    $("span", o).each(function(x,y){ 
    alert(o + " " + y); 
    }); 
}); 

En este ejemplo, o representa el li, mientras que el niño representa yspan. Y con $.click(), se puede obtener el alcance del objeto event:

$("li").click(function(e){ 
    $("span", this).each(function(i,o){ 
    alert(e.target + " " + o); 
    }); 
}); 

Dónde e.target representa el li y o representa el niño span.

10

Para su última MyClass ejemplo, se puede hacer esto:

var myThis=this; 
this.elements.each(function() { myThis.doSomething.apply(myThis, arguments); }); 

En la función que se pasa a each, this se refiere a un objeto jQuery, como ya saben. Si dentro de esa función se obtiene la función de doSomethingmyThis, y luego llama al método se aplica en esa función con la matriz de argumentos (ver el apply function y la arguments variable), entonces this se establecerá en myThis en doSomething.

+1

Eso no funcionará, para cuando llegue a this.doSomething, 'this' ya ha sido reemplazado por jQuery con uno de los elementos. –

+0

Sí, tenía dos problemas cuando originalmente lo publiqué. Lo edité, y ahora debería funcionar. (perdón por eso ...) – icktoofay

0

Otra solución (y mi forma favorita en jQuery) es utilizar el jQuery proporcionado 'e.data' para pasar 'esto'. A continuación, puede hacer esto:

this.element.bind('click', this, function(e) { 
    e.data.myValue; //e.data now references the 'this' that you want 
}); 
0

Se puede crear una referencia al objeto o este se puede utilizar el método with (this). Lo último es extremadamente útil cuando usa controladores de eventos y no tiene forma de pasar una referencia.

MyClass = function() { 
    // More code here ... 
} 

MyClass.prototype.myfunc = function() {  
    // Create a reference 
    var obj = this; 
    this.element.click(function() { 
     // "obj" refers to the original class instance    
     with (this){ 
      // "this" now also refers to the original class instance 
     } 
    }); 

} 
+4

La declaración 'with' debe evitarse debido a la ambigüedad y otros problemas. –

+0

Claro, si puede evitar usarlo, utilice un método más simple, pero sigue siendo válido y útil cuando falla todo lo demás. –

+0

-1: 'with' no cambia el valor de' this' – Bergi

7

que darse cuenta de esto es un hilo viejo, pero tengo una solución que es mucho más elegante, y tiene algunos inconvenientes, aparte del hecho de que no se hace por lo general, como lo he notado.

considerar lo siguiente:

var f=function(){ 
    var context=this; 
}  

f.prototype.test=function(){ 
    return context; 
}  

var fn=new f(); 
fn.test(); 
// should return undefined because the prototype definition 
// took place outside the scope where 'context' is available 

En la función anterior definimos una variable local (contexto). Luego agregamos una función prototípica (prueba) que devuelve la variable local. Como probablemente haya predicho, cuando creamos una instancia de esta función y luego ejecutamos el método de prueba, no devuelve la variable local porque cuando definimos la función prototípica como un miembro de nuestra función principal, estaba fuera del alcance donde el variable local está definida. Este es un problema general con la creación de funciones y la adición de prototipos: no se puede acceder a nada que se haya creado en el ámbito de la función principal.

Para crear métodos que están dentro del alcance de la variable local, tenemos que definir directamente como miembros de la función y deshacerse de la referencia prototípico:

var f=function(){ 
    var context=this;  

    this.test=function(){ 
     console.log(context); 
     return context; 
    }; 
}  

var fn=new(f); 
fn.test(); 
//should return an object that correctly references 'this' 
//in the context of that function;  

fn.test().test().test(); 
//proving that 'this' is the correct reference; 

Usted puede estar preocupado de que, debido a la los métodos no se crean prototípicamente, es posible que las diferentes instancias no estén separadas por datos. Para demostrar que lo son, considere esto:

var f=function(val){ 
    var self=this; 
    this.chain=function(){ 
     return self; 
    };  

    this.checkval=function(){ 
     return val; 
    }; 
}  

var fn1=new f('first value'); 
var fn2=new f('second value');  

fn1.checkval(); 
fn1.chain().chain().checkval(); 
// returns 'first value' indicating that not only does the initiated value remain untouched, 
// one can use the internally stored context reference rigorously without losing sight of local variables.  

fn2.checkval(); 
fn2.chain().chain().checkval(); 
// the fact that this set of tests returns 'second value' 
// proves that they are really referencing separate instances 

Otra forma de utilizar este método es crear singletons. En la mayoría de los casos, nuestras funciones de JavaScript no se crean más de una vez. Si sabe que nunca necesitará una segunda instancia de la misma función, entonces hay una forma abreviada de crearlos. Adviértase, sin embargo: pelusa se quejan de que es una construcción extraña, y cuestiona el uso de la palabra 'nuevo':

fn=new function(val){ 
    var self=this; 
    this.chain=function(){ 
     return self; 
    };   

    this.checkval=function(){ 
     return val; 
    }; 
}  

fn.checkval(); 
fn.chain().chain().checkval(); 

Pros: Los beneficios de utilizar este método para crear objetos de función son abundantes .

  • Hace que su código sea más fácil de leer, ya que sangra los métodos de un objeto de función de una manera que lo hace visualmente más fácil de seguir.
  • Permite el acceso a las variables definidas localmente sólo en los métodos originalmente definidos de esta manera incluso si posteriormente añade funciones prototípicas o incluso funciones miembro de la función a objetos, no puede acceder a las variables locales y cualquier funcionalidad o los datos que almacena en ese nivel permanecen seguros e inaccesibles desde cualquier otro lugar.
  • Permite una forma simple y directa de definir singletons.
  • Le permite almacenar una referencia a 'esto' y mantener esa referencia indefinidamente.

Contras: Hay algunas desventajas de utilizar este método. No pretendo ser exhaustivo :)

  • Debido a que los métodos se definen como miembros al objeto y no prototipos - herencia se puede lograr utilizando definición miembro pero las definiciones no prototípicos. Esto es realmente incorrecto. La misma herencia prototípica se puede lograr actuando en f.constructor.prototype.
+0

Este es un buen método, pero hay otro problema más sutil que puede presentarse en algunos casos. Cuando devuelve un método con su constructor, el operador 'nuevo' ya no devuelve la cadena del prototipo. Es decir, no es una cuestión de estar oculto o sobrescrito, no está allí. Cualquier miembro que hayas tenido en la cadena de prototipos, digamos, de superclases, se ha ido. – dhimes

+0

@dhimes: en realidad, la única razón por la que no puede acceder a la cadena de prototipos es porque ya no tiene acceso a la función de constructor. Excepto que tenemos acceso a él a través de la propiedad ' .constructor'. Pruébelo como prueba: 'a = new function() {}; a.constructor.prototype.b = function() {console.log ('in .b');}; a.b(); ' – Nemesarial

Cuestiones relacionadas