Preservare un riferimento a “questo” nelle funzioni del prototipo JavaScript

Sto iniziando a utilizzare JavaScript prototipale e non riesco a capire come conservare un riferimento all’object principale all’interno di una funzione prototipo quando l’oscilloscopio cambia. Lascia che ti illustri cosa intendo (sto usando jQuery qui):

 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(); 

So che posso mantenere un riferimento all’object principale facendo ciò all’inizio di myfunc :

 var myThis = this; 

e quindi usando myThis.myValue per accedere alla proprietà dell’object principale. Ma cosa succede quando ho un sacco di funzioni prototipo su MyClass ? Devo salvare il riferimento a this all’inizio di ciascuno? Sembra che ci dovrebbe essere un modo più pulito. E che dire di una situazione come questa:

 MyClass = function() { this.elements $('.elements'); this.myValue = 'something'; this.elements.each(this.doSomething); } MyClass.prototype.doSomething = function() { // operate on the element } new MyClass(); 

In tal caso, non posso creare un riferimento all’object principale con var myThis = this; perché anche il valore originale di this nel contesto di doSomething è un object jQuery e non un object MyClass .

Mi è stato suggerito di usare una variabile globale per mantenere il riferimento all’originale, ma mi sembra una pessima idea. Non voglio inquinare lo spazio dei nomi globale e sembra che mi impedisca di istanziare due diversi oggetti MyClass senza che interferiscano l’uno con l’altro.

Eventuali suggerimenti? C’è un modo pulito per fare ciò che sto cercando? O il mio intero modello di progettazione è difettoso?

Per preservare il contesto, il metodo bind è davvero utile, fa ora parte della specifica ECMAScript 5th Edition rilasciata di recente, l’implementazione di questa funzione è semplice (solo 8 righe):

 // 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))); }; }; } 

E potresti usarlo, nel tuo esempio in questo modo:

 MyClass.prototype.myfunc = function() { this.element.click((function() { // ... }).bind(this)); }; 

Un altro esempio:

 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); 

In questo secondo esempio possiamo osservare di più sul comportamento di bind .

Generalmente genera una nuova funzione, che sarà la responsabilità di chiamare la nostra funzione, preservando il contesto della funzione ( this valore), che è definito come il primo argomento di bind .

Il resto degli argomenti viene semplicemente passato alla nostra funzione.

Nota in questo esempio che la funzione fx1 , viene invocata senza alcun contesto object ( obj.method() ), proprio come una semplice chiamata di funzione, in questo tipo di richiamo, la parola chiave inside si riferirà all’object Global, avviserà “test globale”.

Ora, il fx2 è la nuova funzione generata dal metodo di bind , chiamerà la nostra funzione preservando il contesto e passando correttamente gli argomenti, avviserà “obj test 1, 2, 3, 4, 5” perché l’abbiamo invocato aggiungendo il due argomenti addizionali, aveva già vincolato i primi tre.

Per il tuo ultimo esempio di MyClass , potresti fare questo:

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

Nella funzione che viene passata a each , this riferisce a un object jQuery, come già sapete. Se all’interno di quella funzione si ottiene la funzione doSomething da myThis e quindi si chiama il metodo apply su quella funzione con la matrice arguments (si veda la funzione apply e la variabile arguments ), this sarà impostato su myThis in doSomething .

Mi rendo conto che questo è un vecchio thread, ma ho una soluzione che è molto più elegante, e ha alcuni inconvenienti a parte il fatto che non è generalmente fatto, come ho notato.

Considera quanto segue:

 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 

Nella funzione sopra abbiamo definito una variabile locale (contesto). Abbiamo quindi aggiunto una funzione prototipo (test) che restituisce la variabile locale. Come probabilmente avete previsto, quando creiamo un’istanza di questa funzione e quindi eseguiamo il metodo di test, non restituisce la variabile locale perché quando abbiamo definito la funzione prototipale come membro della nostra funzione principale, era fuori dall’ambito in cui la variabile locale è definita. Questo è un problema generale con la creazione di funzioni e quindi l’aggiunta di prototipi ad esso – non è ansible accedere a tutto ciò che è stato creato nell’ambito della funzione principale.

Per creare metodi che rientrano nell’ambito della variabile locale, dobbiamo definirli direttamente come membri della funzione e sbarazzarci del riferimento prototipo:

 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; 

Potresti essere preoccupato che, poiché i metodi non vengono creati in modo prototipico, diverse istanze potrebbero non essere realmente separate dai dati. Per dimostrare che lo sono, considera questo:

 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 

Un altro modo per utilizzare questo metodo è creare singoletti. Più spesso, le nostre funzioni javascript non vengono istanziate più di una volta. Se sai che non avrai mai bisogno di una seconda istanza della stessa funzione, allora c’è un modo stenografico per crearli. Attenzione, tuttavia: lint si lamenterà che si tratta di una strana costruzione e si interroga sull’uso della parola chiave “nuovo”:

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

Pro: I vantaggi dell’utilizzo di questo metodo per creare oggetti funzione sono numerosi.

  • Rende il tuo codice più facile da leggere, poiché indenta i metodi di un object funzione in un modo che lo rende visivamente più facile da seguire.
  • Permette l’accesso alle variabili definite localmente solo nei metodi originariamente definiti in questo modo anche se successivamente si aggiungono funzioni prototipali o addirittura funzioni membro all’object-funzione, non può accedere alle variabili locali e qualsiasi funzionalità o dati memorizzati su quel livello rimane sicuro e inaccessibile da qualsiasi altra parte.
  • Permette un modo semplice e diretto per definire singleton.
  • Ti permette di memorizzare un riferimento a “questo” e di mantenere quel riferimento indefinitamente.

Contro: Ci sono alcuni svantaggi nell’usare questo metodo. Non pretendo di essere esauriente 🙂

  • Poiché i metodi sono definiti come membri dell’object e non prototipi, l’ereditarietà può essere ottenuta utilizzando la definizione dei membri ma non le definizioni prototipiche. Questo è in realtà sbagliato. La stessa eredità prototipica può essere ottenuta agendo su f.constructor.prototype .

È ansible impostare l’ambito utilizzando le funzioni call () e apply ()

Dato che stai usando jQuery, vale la pena notare che this è già gestito da jQuery stesso:

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

In questo esempio, o rappresenta il li , mentre y rappresenta l’ span child. E con $.click() , puoi ottenere l’ambito dall’object event :

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

Dove e.target rappresenta il li e o rappresenta l’ e.target child.

È ansible creare un riferimento a questo object o utilizzare il metodo with (this) . Il successivo è estremamente utile quando si utilizzano gestori di eventi e non si ha modo di trasmettere un riferimento.

 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 } }); } 

Un’altra soluzione (e il mio modo preferito in jQuery) è usare il jQuery fornito ‘e.data’ per passare ‘questo’. Quindi puoi fare questo:

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