2010-07-17 3 views
23

sto leggendo sito dello sviluppatore di Mozilla sulle chiusure, e ho notato in loro esempio per errori comuni, hanno avuto questo codice:Javascript chiusure - questione portata variabile

<p id="help">Helpful notes will appear here</p> 
<p>E-mail: <input type="text" id="email" name="email"></p> 
<p>Name: <input type="text" id="name" name="name"></p> 
<p>Age: <input type="text" id="age" name="age"></p> 

e

function showHelp(help) { 
    document.getElementById('help').innerHTML = help; 
} 

function setupHelp() { 
    var helpText = [ 
     {'id': 'email', 'help': 'Your e-mail address'}, 
     {'id': 'name', 'help': 'Your full name'}, 
     {'id': 'age', 'help': 'Your age (you must be over 16)'} 
    ]; 

    for (var i = 0; i < helpText.length; i++) { 
    var item = helpText[i]; 
    document.getElementById(item.id).onfocus = function() { 
     showHelp(item.help); 
    } 
    } 
} 

e hanno detto che per l'evento onFocus, il codice mostrerebbe solo l'aiuto per l'ultimo elemento, poiché tutte le funzioni anonime assegnate all'evento onFocus hanno una chiusura attorno alla variabile "item", il che ha senso perché nelle variabili JavaScript non hanno ambito di blocco. La soluzione era usare 'let item = ...' invece, poiché ha un ambito di blocco.

Tuttavia, quello che mi chiedo è perché non si può dichiarare 'elemento var' proprio sopra il ciclo for? Quindi ha lo scopo di setupHelp(), e ad ogni iterazione si assegna un valore diverso, che verrà quindi catturato come valore corrente nella chiusura ... giusto?

+2

javascript ha 'let'? cercare ... – Kobi

+1

@Kobi: È un'estensione specifica di Mozilla per JavaScript. Vedi [questo] (https://developer.mozilla.org/en/new_in_javascript_1.7). –

+0

Se è specifico per Mozilla, vuol dire che dovrei evitare di usarlo? – Nick

risposta

26

È perché al momento in cui viene valutato item.help, il ciclo dovrebbe essere completato nella sua interezza. Invece, puoi farlo con una chiusura:

for (var i = 0; i < helpText.length; i++) { 
    document.getElementById(helpText[i].id).onfocus = function(item) { 
      return function() {showHelp(item.help);}; 
     }(helpText[i]); 
} 

JavaScript non ha scope di blocco ma ha scope di funzioni. Creando una chiusura, acquisiamo il riferimento a helpText[i] in modo permanente.

+3

Questa è la soluzione accettata standard fare una funzione che restituisce la funzione del gestore di eventi, ma anche di scoping variabile (s) che si bisogno di tenere traccia di lato interessante nota, di jQuery [ '$ .each()'] (http://api.jquery.com/jQuery.each) fa questo da sola, mentre passa l'indice, e la voce in una funzione si specifica, così scoping l'interno del ciclo reso facile. – gnarf

1

Anche se è dichiarato al di fuori del ciclo for, ognuna delle funzioni anonime farà ancora riferimento alla stessa variabile, quindi dopo il ciclo, punteranno sempre al valore finale dell'elemento.

2

nuovi ambiti sono solo creata in function blocchi (e with, ma non usare questo). I cicli come for non creano nuovi ambiti.

Quindi, anche se si dichiarasse la variabile fuori dal ciclo, si verificherà lo stesso identico problema.

+2

Basta notare che 'with' non crea un nuovo ambiente lessicale, ad esempio, se * dichiari * una variabile all'interno di un blocco' with', la variabile sarà associata al suo ambito genitore (sarà * issato *) . 'With' solo introduce un oggetto davanti alla catena di portata, [more info] (http://stackoverflow.com/questions/2742819/) – CMS

20

Una chiusura è una funzione e l'ambito di tale funzione.

Aiuta a capire come Javascript implementa lo scope in questo caso. È, infatti, solo una serie di dizionari nidificati. Considerate questo codice:

var global1 = "foo"; 

function myFunc() { 
    var x = 0; 
    global1 = "bar"; 
} 

myFunc(); 

All'avvio del programma in esecuzione, si dispone di un unico dizionario ambito, il dizionario globale, che potrebbe avere un certo numero di cose definite in esso:

{ global1: "foo", myFunc:<function code> } 

Dire si chiama myFunc , che ha una variabile locale x. Viene creato un nuovo ambito per l'esecuzione di questa funzione. L'ambito locale della funzione è simile al seguente:

{ x: 0 } 

Contiene anche un riferimento all'ambito principale. Quindi l'intero scopo della funzione è il seguente:

{ x: 0, parentScope: { global1: "foo", myFunc:<function code> } } 

Ciò consente a myFunc di modificare global1. In Javascript, ogni volta che si tenta di assegnare un valore a una variabile, prima controlla l'ambito locale per il nome della variabile. Se non viene trovato, controlla genitoreScope e genitoreScope di tale scope, ecc. Fino a quando la variabile non viene trovata.

Una chiusura è letteralmente una funzione più un puntatore all'ambito di questa funzione (che contiene un puntatore all'ambito principale e così via). Quindi, nel tuo esempio, dopo il ciclo for ha terminato l'esecuzione, la portata potrebbe essere simile a questo:

setupHelpScope = { 
    helpText:<...>, 
    i: 3, 
    item: {'id': 'age', 'help': 'Your age (you must be over 16)'}, 
    parentScope: <...> 
} 

Ogni chiusura di creare punterà a questo singolo oggetto ambito. Se dovessimo elencare ogni chiusura che si è creato, che sarebbe simile a questa:

[anonymousFunction1, setupHelpScope] 
[anonymousFunction2, setupHelpScope] 
[anonymousFunction3, setupHelpScope] 

Quando una di queste funzioni viene eseguito, si utilizza l'oggetto ambito che è stata approvata - in questo caso, è la stessa portata oggetto per ogni funzione! Ognuno guarderà la stessa variabile item e vedrà lo stesso valore, che è l'ultimo impostato dal ciclo for.

Per rispondere alla tua domanda, non importa se aggiungi var item sopra il ciclo for o al suo interno. Poiché i loop for non creano il proprio ambito, item verrà archiviato nel dizionario dell'ambito della funzione corrente, ovvero setupHelpScope. Gli involucri generati all'interno del ciclo for punteranno sempre a setupHelpScope.

Alcune note importanti:

  • Questo comportamento si verifica perché, in Javascript, for loop non hanno un proprio campo d'applicazione - che basta usare la portata della funzione di inclusione. Questo vale anche per if, while, switch, ecc. Se si trattasse di C#, d'altra parte, un nuovo oggetto di ambito verrà creato per ciascun ciclo e ogni chiusura conterrà un puntatore al proprio ambito univoco.
  • Si noti che se anonymousFunction1 modifica una variabile nel proprio ambito, modifica quella variabile per le altre funzioni anonime. Questo può portare ad alcune interazioni davvero bizzarre.
  • Gli ambiti sono solo oggetti, come quelli con cui si programma. Specificamente, sono dizionari. La macchina virtuale JS gestisce la loro cancellazione dalla memoria proprio come qualsiasi altra cosa - con il garbage collector. Per questo motivo, l'uso eccessivo di chiusure può creare un reale rigonfiamento della memoria. Poiché una chiusura contiene un puntatore a un oggetto ambito (che a sua volta contiene un puntatore al suo oggetto ambito genitore e avanti e indietro), l'intera catena dell'ambito non può essere raccolta da garbage collection e deve rimanere in memoria.

Ulteriori approfondimenti:

+2

Questo è un ottimo modo per descrivere scoping JavaScript - tuttavia non ha fornito/spiegare le soluzioni al problema. Per salvare una copia della variabile così com'è, tutto ciò che devi fare è creare un nuovo scope, facendo una funzione per restituire una funzione: '(function (item) {return function() {...}}) (oggetto); 'che ha accesso all'elemento' ambito'. È inoltre possibile definire una funzione in precedenza nel codice come: 'funzione genHelpFocus (voce) {funzione di ritorno() {showHelp (item.html); }} 'per aiutare a prevenire il gonfiore del leakage dell'oscilloscopio. – gnarf

+0

Questa è la spiegazione più chiara che ho sentito. Grazie – Adam

3

mi rendo conto che la domanda iniziale è di cinque anni ...Ma si potrebbe anche solo legano una portata diversa/speciale per la funzione di callback si assegna ad ogni elemento:

// Function only exists once in memory 
function doOnFocus() { 
    // ...but you make the assumption that it'll be called with 
    // the right "this" (context) 
    var item = helpText[this.index]; 
    showHelp(item.help); 
}; 

for (var i = 0; i < helpText.length; i++) { 
    // Create the special context that the callback function 
    // will be called with. This context will have an attr "i" 
    // whose value is the current value of "i" in this loop in 
    // each iteration 
    var context = {index: i}; 

    document.getElementById(helpText[i].id).onfocus = doOnFocus.bind(context); 
} 

Se si desidera una battuta (o vicino ad esso):

// Kind of messy... 
for (var i = 0; i < helpText.length; i++) { 
    document.getElementById(helpText[i].id).onfocus = function(){ 
     showHelp(helpText[this.index].help); 
    }.bind({index: i}); 
} 

O meglio ancora, è possibile utilizzare EcmaScript 5.1 di array.prototype.forEach, che risolve il problema spazio per voi.

helpText.forEach(function(help){ 
    document.getElementById(help.id).onfocus = function(){ 
     showHelp(help); 
    }; 
});