2010-07-17 13 views
23

estoy leyendo el sitio del desarrollador de Mozilla sobre el cierre, y me di cuenta en su ejemplo para errores comunes, que tenían este código:cierres de Javascript - pregunta ámbito de las variables

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

y

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

y dijeron que para el evento onFocus, el código solo mostraría ayuda para el último elemento porque todas las funciones anónimas asignadas al evento onFocus tienen un cierre alrededor de la variable 'item', lo cual tiene sentido porque en JavaScript las variables no tienen alcance del bloque. La solución fue usar 'let item = ...' en su lugar, para entonces tiene alcance de bloque.

Sin embargo, lo que me pregunto es por qué no podría declarar 'var item' justo encima del bucle for? Luego tiene el alcance de setupHelp(), y cada iteración le está asignando un valor diferente, que luego sería capturado como su valor actual en el cierre ... ¿verdad?

+2

javascript has 'let'? mirando hacia arriba ... – Kobi

+1

@Kobi: Es una extensión específica de Mozilla para JavaScript. Ver [esto] (https://developer.mozilla.org/en/new_in_javascript_1.7). –

+0

Si es específico de Mozilla, ¿significa eso que debo evitar usarlo? – Nick

Respuesta

26

Es porque en el momento en que se evalúa item.help, el ciclo se habrá completado en su totalidad. En cambio, puede hacer esto con un cierre:

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

JavaScript no tiene alcance de bloque pero sí tiene ámbito de función. Al crear un cierre, estamos capturando la referencia a helpText[i] permanentemente.

+3

Esta es la solución estándar aceptada, realice una función que devuelva la función del controlador de eventos, mientras que también analiza las variables que usted Necesito hacer un seguimiento de. Nota interesante, jQuery's ['$ .each()'] (http://api.jquery.com/jQuery.each) hace esto por sí solo, a medida que pasa el índice, y el ítem en una función que usted especifique, por lo tanto, es fácil explorar el interior del bucle. – gnarf

1

Incluso si se declara fuera del bucle for, cada una de las funciones anónimas seguirá haciendo referencia a la misma variable, por lo que después del bucle, todas seguirán apuntando al valor final del elemento.

2

nuevos ámbitos son única creada en function bloques (y with, pero que no utilizan). Los bucles como for no crean nuevos ámbitos.

Así que incluso si declara la variable fuera del ciclo, se encontrará exactamente con el mismo problema.

+2

Simplemente tenga en cuenta que 'con' no crea un nuevo entorno léxico, por ejemplo, si * declara * una variable dentro de un bloque' con', la variable estará ligada a su alcance principal (será * izada *) . 'con' solo presenta un objeto al frente de la cadena de alcance, [más información] (http://stackoverflow.com/questions/2742819/) – CMS

20

Un cierre es una función y el entorno del ámbito de esa función.

Ayuda a comprender cómo Javascript implementa el alcance en este caso. De hecho, es solo una serie de diccionarios anidados. Considere este código:

var global1 = "foo"; 

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

myFunc(); 

Cuando el programa se pone en marcha, tiene un diccionario ámbito individual, el diccionario mundial, lo que podría tener un número de cosas definidas en ella:

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

Diga usted llama myFunc , que tiene una variable local x. Se crea un nuevo alcance para la ejecución de esta función. El ámbito local de la función se ve así:

{ x: 0 } 

También contiene una referencia a su ámbito primario. Por lo tanto, todo el alcance de la función es el siguiente:

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

Esto permite que myFunc modifique global1. En Javascript, siempre que intenta asignar un valor a una variable, primero verifica el alcance local para el nombre de la variable. Si no se encuentra, comprueba el parentScope y el parentScope del alcance, etc. hasta que se encuentre la variable.

Un cierre es literalmente una función más un puntero al alcance de esa función (que contiene un puntero a su alcance principal, y así sucesivamente). Así, en su ejemplo, después del bucle for ha finalizado la ejecución, el alcance podría tener este aspecto:

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

Cada cierre se crea apuntará a este objeto alcance individual. Si tuviéramos que enumerar cada cierre que ha creado, se vería algo como esto:

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

Cuando cualquiera de estas funciones se ejecuta, utiliza el objeto alcance que se aprobó - en este caso, es el mismo alcance objeto para cada función! Cada uno mirará la misma variable item y verá el mismo valor, que es el último establecido por su bucle for.

Para responder a su pregunta, no importa si agrega var item sobre el lazo for o dentro de él. Como los bucles for no crean su propio ámbito, item se almacenará en el diccionario de alcance de la función actual, que es setupHelpScope. Los gabinetes generados dentro del bucle for siempre apuntarán al setupHelpScope.

Algunas notas importantes:

  • Este comportamiento se produce porque, en Javascript, for bucles no tienen su propio ámbito - sólo tiene que utilizar el alcance de la función de cerramiento. Esto también es cierto para if, while, switch, etc. Si esto fuera C#, por otro lado, se crearía un nuevo objeto de ámbito para cada ciclo, y cada cierre contendría un puntero a su propio alcance único.
  • Observe que si anonymousFunction1 modifica una variable en su ámbito, modifica esa variable para las demás funciones anónimas. Esto puede conducir a algunas interacciones realmente extrañas.
  • Los ámbitos son solo objetos, como los que usted programa. Específicamente, son diccionarios. La máquina virtual JS gestiona su eliminación de la memoria como cualquier otra cosa, con el recolector de basura. Por esta razón, el uso excesivo de cierres puede crear una hinchazón de memoria real. Dado que un cierre contiene un puntero a un objeto de ámbito (que a su vez contiene un puntero a su objeto de ámbito principal y así sucesivamente), no se puede recolectar la cadena completa del alcance y debe permanecer en la memoria.

Más información:

+2

Esta es una gran manera de describir el alcance de JavaScript. Sin embargo, usted no proporcionó explica las soluciones al problema. Para guardar una copia de la variable tal como está, todo lo que necesita hacer es crear un nuevo ámbito, haciendo una función para devolver una función: '(función (elemento) {función de retorno() {...}}) (artículo); 'que tiene acceso al elemento' con alcance '. También puede definir una función anteriormente en el código como: 'function genHelpFocus (item) {return function() {showHelp (item.html); }} 'para ayudar a prevenir la distensión de la filtración del alcance. – gnarf

+0

Esa es la explicación más clara que he escuchado. Gracias – Adam

3

realizo la pregunta original es de cinco años de edad ...Pero se puede también acaba de unen un ámbito diferente/especial a la función de devolución de llamada que asignar a cada 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); 
} 

Si desea una sola línea (o cerca de ella):

// 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 mejor aún, puede usar el array.prototype.forEach de EcmaScript 5.1, que soluciona el problema de alcance por usted.

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