JavaScript es un lenguaje muy orientado a funciones. Nos da mucha libertad. Una función se puede crear en cualquier momento, pasarse como argumento a otra función y luego llamarse desde un lugar de código totalmente diferente.
Ya sabemos que una función puede acceder a variables fuera de ella (variables "externas").
Pero, ¿qué sucede si las variables externas cambian desde que se crea una función? ¿La función obtendrá valores más nuevos o más antiguos?
¿Y qué pasa si una función se pasa como argumento y se llama desde otro lugar del código? ¿Tendrá acceso a las variables externas en el nuevo lugar?
Ampliemos nuestro conocimiento para comprender estos escenarios y otros más complejos.
Hablaremos de las variables let/const
aquí.
En JavaScript, hay 3 formas de declarar una variable: let
, const
(las modernas) y var
(el remanente del pasado).
En este artículo usaremos variables let
en ejemplos.
Las variables declaradas con const
se comportan igual, por lo que este artículo también trata sobre const
.
La antigua var
tiene algunas diferencias notables, que se tratarán en el artículo La antigua "var".
Si una variable se declara dentro de un bloque de código {...}
, solo es visible dentro de ese bloque.
Por ejemplo:
{ // hacer un trabajo con variables locales que no deberían verse afuera dejar mensaje = "Hola"; // solo visible en este bloque alerta(mensaje); // Hola } alerta(mensaje); // Error: el mensaje no está definido
Podemos usar esto para aislar un fragmento de código que hace su propia tarea, con variables que solo le pertenecen a él:
{ // mostrar mensaje dejar mensaje = "Hola"; alerta(mensaje); } { //muestra otro mensaje let mensaje = "Adiós"; alerta(mensaje); }
Habría un error sin bloques.
Tenga en cuenta que sin bloques separados habría un error si usamos let
con el nombre de variable existente:
// mostrar mensaje dejar mensaje = "Hola"; alerta(mensaje); //muestra otro mensaje let mensaje = "Adiós"; // Error: variable ya declarada alerta(mensaje);
Para if
, for
, while
, etc., las variables declaradas en {...}
también son visibles solo dentro:
si (verdadero) { let frase = "¡Hola!"; alerta(frase); // ¡Hola! } alerta(frase); // ¡Error, no existe tal variable!
Aquí, después de if
finalice, la alert
a continuación no verá la phrase
, de ahí el error.
Eso es genial, ya que nos permite crear variables locales de bloque, específicas para una rama if
.
Lo mismo ocurre con los bucles for
y while
:
para (sea i = 0; i < 3; i++) { // la variable i sólo es visible dentro de esto durante alerta(yo); // 0, luego 1, luego 2 } alerta(yo); // Error, no existe tal variable
Visualmente, let i
está fuera de {...}
. Pero la construcción for
es especial aquí: la variable declarada dentro de ella se considera parte del bloque.
Una función se denomina "anisada" cuando se crea dentro de otra función.
Es fácilmente posible hacer esto con JavaScript.
Podemos usarlo para organizar nuestro código, así:
función decirHolaAdiós(nombre, apellido) { // función anidada auxiliar para usar a continuación función obtenerNombreCompleto() { devolver nombre + " " + apellido; } alerta( "Hola, " + getFullName() ); alert( "Adiós, " + getFullName() ); }
Aquí la función anidada getFullName()
está hecha por conveniencia. Puede acceder a las variables externas y, por lo tanto, puede devolver el nombre completo. Las funciones anidadas son bastante comunes en JavaScript.
Lo que es mucho más interesante es que se puede devolver una función anidada: ya sea como propiedad de un nuevo objeto o como resultado de ella misma. Luego se puede utilizar en otro lugar. No importa dónde, todavía tiene acceso a las mismas variables externas.
A continuación, makeCounter
crea la función "contador" que devuelve el siguiente número en cada invocación:
función crearContador() { deja contar = 0; función de retorno() { recuento de retorno ++; }; } let contador = makeCounter(); alerta( contador() ); // 0 alerta( contador() ); // 1 alerta( contador() ); // 2
A pesar de ser simples, las variantes ligeramente modificadas de ese código tienen usos prácticos, por ejemplo, como generador de números aleatorios para generar valores aleatorios para pruebas automatizadas.
¿Cómo funciona esto? Si creamos varios contadores, ¿serán independientes? ¿Qué está pasando con las variables aquí?
Comprender este tipo de cosas es excelente para el conocimiento general de JavaScript y beneficioso para escenarios más complejos. Así que profundicemos un poco.
¡Aquí hay dragones!
La explicación técnica en profundidad está por delante.
En la medida en que me gustaría evitar detalles del lenguaje de bajo nivel, cualquier comprensión sin ellos sería deficiente e incompleta, así que prepárate.
Para mayor claridad, la explicación se divide en varios pasos.
En JavaScript, cada función en ejecución, bloque de código {...}
y el script en su conjunto tienen un objeto asociado interno (oculto) conocido como Entorno Léxico .
El objeto Lexical Environment consta de dos partes:
Registro de entorno : un objeto que almacena todas las variables locales como sus propiedades (y alguna otra información como el valor de this
).
Una referencia al entorno léxico externo , el asociado con el código externo.
Una “variable” es sólo una propiedad del objeto interno especial, Environment Record
. "Obtener o cambiar una variable" significa "obtener o cambiar una propiedad de ese objeto".
En este código simple y sin funciones, solo hay un entorno léxico:
Este es el llamado entorno léxico global , asociado con todo el guión.
En la imagen de arriba, el rectángulo significa Registro de entorno (almacenamiento de variables) y la flecha significa la referencia exterior. El entorno léxico global no tiene referencia externa, por eso la flecha apunta a null
.
A medida que el código comienza a ejecutarse y continúa, el entorno léxico cambia.
Aquí hay un código un poco más largo:
Los rectángulos en el lado derecho demuestran cómo cambia el entorno léxico global durante la ejecución:
Cuando se inicia el script, el entorno léxico se completa previamente con todas las variables declaradas.
Inicialmente, se encuentran en el estado "No inicializado". Ese es un estado interno especial, significa que el motor conoce la variable, pero no se puede hacer referencia a ella hasta que se haya declarado con let
. Es casi lo mismo que si la variable no existiera.
Luego let phrase
. Aún no hay ninguna asignación, por lo que su valor no está undefined
. Podemos usar la variable a partir de este momento.
phrase
se le asigna un valor.
phrase
cambia el valor.
Todo parece sencillo por ahora, ¿verdad?
Una variable es una propiedad de un objeto interno especial, asociado con el bloque/función/script que se está ejecutando actualmente.
Trabajar con variables es en realidad trabajar con las propiedades de ese objeto.
El entorno léxico es un objeto de especificación.
El “entorno léxico” es un objeto de especificación: sólo existe “teóricamente” en la especificación del lenguaje para describir cómo funcionan las cosas. No podemos incluir este objeto en nuestro código y manipularlo directamente.
Los motores de JavaScript también pueden optimizarlo, descartar variables que no se utilizan para ahorrar memoria y realizar otros trucos internos, siempre que el comportamiento visible siga siendo el descrito.
Una función también es un valor, como una variable.
La diferencia es que una Declaración de función se inicializa por completo al instante.
Cuando se crea un entorno léxico, una declaración de función se convierte inmediatamente en una función lista para usar (a diferencia de let
, que no se puede utilizar hasta la declaración).
Es por eso que podemos usar una función, declarada como Declaración de función, incluso antes de la declaración misma.
Por ejemplo, aquí está el estado inicial del entorno léxico global cuando agregamos una función:
Naturalmente, este comportamiento solo se aplica a declaraciones de funciones, no a expresiones de funciones donde asignamos una función a una variable, como let say = function(name)...
.
Cuando se ejecuta una función, al comienzo de la llamada, se crea automáticamente un nuevo entorno léxico para almacenar variables locales y parámetros de la llamada.
Por ejemplo, say("John")
, se ve así (la ejecución se realiza en la línea marcada con una flecha):
Durante la llamada a la función tenemos dos Entornos Léxicos: el interno (para la llamada a la función) y el externo (global):
El entorno léxico interno corresponde a la ejecución actual de say
. Tiene una única propiedad: name
, el argumento de la función. Llamamos say("John")
, por lo que el valor del name
es "John"
.
El entorno léxico exterior es el entorno léxico global. Tiene la phrase
variable y la función en sí.
El entorno léxico interno tiene una referencia al outer
.
Cuando el código quiere acceder a una variable, primero se busca el entorno léxico interno, luego el externo, luego el más externo y así sucesivamente hasta el global.
Si una variable no se encuentra en ninguna parte, se trata de un error en modo estricto (sin use strict
, una asignación a una variable no existente crea una nueva variable global, por compatibilidad con el código antiguo).
En este ejemplo, la búsqueda se realiza de la siguiente manera:
Para la variable name
, la alert
dentro say
la encuentra inmediatamente en el entorno léxico interno.
Cuando quiere acceder a phrase
, entonces no hay ninguna phrase
localmente, por lo que sigue la referencia al entorno léxico externo y la encuentra allí.
Volvamos al ejemplo makeCounter
.
función crearContador() { deja contar = 0; función de retorno() { recuento de retorno ++; }; } let contador = makeCounter();
Al comienzo de cada llamada makeCounter()
, se crea un nuevo objeto de entorno léxico para almacenar variables para esta ejecución makeCounter
.
Entonces tenemos dos entornos léxicos anidados, como en el ejemplo anterior:
La diferencia es que, durante la ejecución de makeCounter()
, se crea una pequeña función anidada de una sola línea: return count++
. Aún no lo ejecutamos, solo lo creamos.
Todas las funciones recuerdan el entorno léxico en el que fueron realizadas. Técnicamente, no hay magia aquí: todas las funciones tienen la propiedad oculta llamada [[Environment]]
, que mantiene la referencia al entorno léxico donde se creó la función:
Entonces, counter.[[Environment]]
tiene la referencia a {count: 0}
Entorno léxico. Así es como la función recuerda dónde fue creada, sin importar dónde se llame. La referencia [[Environment]]
se establece una vez por todas en el momento de la creación de la función.
Más tarde, cuando se llama counter()
, se crea un nuevo entorno léxico para la llamada y su referencia de entorno léxico externo se toma de counter.[[Environment]]
:
Ahora, cuando el código dentro de counter()
busca la variable count
, primero busca en su propio entorno léxico (vacío, ya que no hay variables locales allí), luego en el entorno léxico de la llamada externa makeCounter()
, donde lo encuentra y lo cambia. .
Una variable se actualiza en el entorno léxico donde vive.
Aquí está el estado después de la ejecución:
Si llamamos counter()
varias veces, la variable count
aumentará a 2
, 3
y así sucesivamente, en el mismo lugar.
Cierre
Existe un término de programación general "cierre" que los desarrolladores generalmente deberían conocer.
Un cierre es una función que recuerda sus variables externas y puede acceder a ellas. En algunos lenguajes, eso no es posible, o una función debe escribirse de una manera especial para que esto suceda. Pero como se explicó anteriormente, en JavaScript, todas las funciones son naturalmente cierres (solo hay una excepción, que se tratará en la sintaxis de la "nueva función").
Es decir: recuerdan automáticamente dónde fueron creados usando una propiedad [[Environment]]
oculta, y luego su código puede acceder a variables externas.
Cuando en una entrevista, un desarrollador frontend recibe una pregunta sobre "¿qué es un cierre?", una respuesta válida sería una definición de cierre y una explicación de que todas las funciones en JavaScript son cierres, y tal vez algunas palabras más sobre detalles técnicos: la propiedad [[Environment]]
y cómo funcionan los entornos léxicos.
Por lo general, un entorno léxico se elimina de la memoria con todas las variables una vez finalizada la llamada a la función. Eso es porque no hay referencias al respecto. Como cualquier objeto JavaScript, sólo se mantiene en la memoria mientras es accesible.
Sin embargo, si hay una función anidada a la que aún se puede acceder después del final de una función, entonces tiene la propiedad [[Environment]]
que hace referencia al entorno léxico.
En ese caso, aún se puede acceder al entorno léxico incluso después de completar la función, por lo que permanece vivo.
Por ejemplo:
función f() { dejar valor = 123; función de retorno() { alerta(valor); } } sea g = f(); // g.[[Environment]] almacena una referencia al entorno léxico // de la llamada f() correspondiente
Tenga en cuenta que si se llama f()
muchas veces y las funciones resultantes se guardan, todos los objetos del entorno léxico correspondientes también se conservarán en la memoria. En el siguiente código, los 3:
función f() { let valor = Math.random(); función de retorno() { alerta(valor); }; } // 3 funciones en una matriz, cada una de ellas vinculada al entorno léxico // de la ejecución f() correspondiente let arr = [f(), f(), f()];
Un objeto de entorno léxico muere cuando se vuelve inalcanzable (como cualquier otro objeto). En otras palabras, existe sólo mientras hay al menos una función anidada que hace referencia a ella.
En el siguiente código, después de eliminar la función anidada, el entorno léxico que la contiene (y por lo tanto el value
) se limpia de la memoria:
función f() { dejar valor = 123; función de retorno() { alerta(valor); } } sea g = f(); // mientras exista la función g, el valor permanece en la memoria g = nulo; // ...y ahora se limpia la memoria
Como hemos visto, en teoría, mientras una función está viva, todas las variables externas también se conservan.
Pero en la práctica, los motores JavaScript intentan optimizar eso. Analizan el uso de variables y, si del código resulta obvio que una variable externa no se utiliza, se elimina.
Un efecto secundario importante en V8 (Chrome, Edge, Opera) es que dicha variable dejará de estar disponible durante la depuración.
Intente ejecutar el siguiente ejemplo en Chrome con las Herramientas de desarrollo abiertas.
Cuando se detenga, en la consola escriba alert(value)
.
función f() { let valor = Math.random(); función g() { depurador; // en la consola: escriba alerta(valor); ¡No existe tal variable! } devolver g; } sea g = f(); gramo();
Como puede ver, ¡no existe tal variable! En teoría, debería ser accesible, pero el motor lo optimizó.
Esto puede llevar a problemas de depuración divertidos (si no que consumen tanto tiempo). Uno de ellos: podemos ver una variable externa con el mismo nombre en lugar de la esperada:
let valor = "¡Sorpresa!"; función f() { let valor = "el valor más cercano"; función g() { depurador; // en la consola: escriba alerta(valor); ¡Sorpresa! } devolver g; } sea g = f(); gramo();
Es bueno saber esta característica del V8. Si está depurando con Chrome/Edge/Opera, tarde o temprano lo encontrará.
Esto no es un error en el depurador, sino más bien una característica especial de V8. Quizás en algún momento se cambie. Siempre puedes comprobarlo ejecutando los ejemplos de esta página.
importancia: 5
La función decir Hola usa un nombre de variable externa. Cuando se ejecute la función, ¿qué valor utilizará?
let nombre = "Juan"; función decir Hola() { alerta("Hola, " + nombre); } nombre = "Pete"; decir Hola(); // ¿Qué mostrará: "John" o "Pete"?
Estas situaciones son comunes tanto en el desarrollo del lado del navegador como del servidor. Se puede programar una función para que se ejecute más tarde de su creación, por ejemplo, después de una acción del usuario o una solicitud de red.
Entonces, la pregunta es: ¿recoge los últimos cambios?
La respuesta es: Pete .
Una función obtiene las variables externas tal como están ahora, utiliza los valores más recientes.
Los valores de las variables antiguas no se guardan en ninguna parte. Cuando una función quiere una variable, toma el valor actual de su propio entorno léxico o del externo.
importancia: 5
La siguiente función makeWorker
crea otra función y la devuelve. Esa nueva función se puede llamar desde otro lugar.
¿Tendrá acceso a las variables externas desde su lugar de creación, desde el lugar de invocación, o desde ambos?
función hacerTrabajador() { let nombre = "Pete"; función de retorno() { alerta(nombre); }; } let nombre = "Juan"; // crear una función dejar trabajar = makeWorker(); // llámalo trabajar(); // ¿qué mostrará?
¿Qué valor mostrará? ¿“Pete” o “John”?
La respuesta es: Pete .
La función work()
en el código siguiente obtiene name
del lugar de su origen a través de la referencia del entorno léxico externo:
Entonces, el resultado aquí es "Pete"
.
Pero si no hubiera ningún let name
en makeWorker()
, entonces la búsqueda saldría y tomaría la variable global como podemos ver en la cadena anterior. En ese caso el resultado sería "John"
.
importancia: 5
Aquí creamos dos contadores: counter
y counter2
usando la misma función makeCounter
.
¿Son independientes? ¿Qué va a mostrar el segundo contador? 0,1
o 2,3
o algo más?
función crearContador() { deja contar = 0; función de retorno() { recuento de retorno ++; }; } let contador = makeCounter(); let contador2 = makeCounter(); alerta( contador() ); // 0 alerta( contador() ); // 1 alerta( contador2() ); // ? alerta( contador2() ); // ?
La respuesta: 0,1.
Las funciones counter
y counter2
se crean mediante diferentes invocaciones de makeCounter
.
Entonces tienen entornos léxicos externos independientes, cada uno tiene su propio count
.
importancia: 5
Aquí se crea un objeto contador con la ayuda de la función constructora.
¿Funcionará? ¿Qué mostrará?
función Contador() { deja contar = 0; this.up = función() { devolver ++cuenta; }; this.abajo = función() { retorno --cuenta; }; } let contador = nuevo contador(); alerta( contador.up() ); // ? alerta( contador.up() ); // ? alerta( contador.down() ); // ?
Seguramente funcionará bien.
Ambas funciones anidadas se crean dentro del mismo entorno léxico externo, por lo que comparten acceso a la misma variable count
:
función Contador() { deja contar = 0; this.up = función() { devolver ++cuenta; }; this.abajo = función() { retorno --cuenta; }; } let contador = nuevo contador(); alerta( contador.up() ); // 1 alerta( contador.up() ); // 2 alerta( contador.down() ); // 1
importancia: 5
Mira el código. ¿Cuál será el resultado de la llamada en la última línea?
let frase = "Hola"; si (verdadero) { dejar usuario = "Juan"; función decir Hola() { alerta(`${frase}, ${usuario}`); } } decir Hola();
El resultado es un error .
La función sayHi
se declara dentro de if
, por lo que solo vive dentro de él. No hay sayHi
afuera.
importancia: 4
Escribe una función sum
que funcione así: sum(a)(b) = a+b
.
Sí, exactamente de esta manera, usando paréntesis dobles (no es un error tipográfico).
Por ejemplo:
suma(1)(2) = 3 suma(5)(-1) = 4
Para que el segundo paréntesis funcione, los primeros deben devolver una función.
Como esto:
función suma(a) { función de retorno (b) { devolver a + b; // toma "a" del entorno léxico externo }; } alerta( suma(1)(2) ); // 3 alerta( suma(5)(-1) ); // 4
importancia: 4
¿Cuál será el resultado de este código?
sea x = 1; función función() { consola.log(x); // ? sea x = 2; } función();
PD: Hay un peligro en esta tarea. La solución no es obvia.
El resultado es: error .
Intenta ejecutarlo:
sea x = 1; función función() { consola.log(x); // ReferenceError: No se puede acceder a 'x' antes de la inicialización sea x = 2; } función();
En este ejemplo podemos observar la peculiar diferencia entre una variable “no existente” y “no inicializada”.
Como habrás leído en el artículo Alcance de la variable, cierre, una variable comienza en el estado “no inicializado” desde el momento en que la ejecución ingresa a un bloque de código (o una función). Y permanece sin inicializar hasta la declaración let
correspondiente.
En otras palabras, técnicamente existe una variable, pero no se puede usar antes de let
.
El código anterior lo demuestra.
función función() { // la variable local x es conocida por el motor desde el comienzo de la función, // pero "no inicializado" (inutilizable) hasta que se deje ("zona muerta") // de ahí el error consola.log(x); // ReferenceError: No se puede acceder a 'x' antes de la inicialización sea x = 2; }
Esta zona de inutilizabilidad temporal de una variable (desde el comienzo del bloque de código hasta let
) a veces se denomina "zona muerta".
importancia: 5
Tenemos un método incorporado arr.filter(f)
para matrices. Filtra todos los elementos a través de la función f
. Si devuelve true
, entonces ese elemento se devuelve en la matriz resultante.
Cree un conjunto de filtros "listos para usar":
inBetween(a, b)
– entre a
y b
o igual a ellos (inclusive).
inArray([...])
– en la matriz dada.
El uso debe ser así:
arr.filter(inBetween(3,6))
– selecciona solo valores entre 3 y 6.
arr.filter(inArray([1,2,3]))
– selecciona solo elementos que coinciden con uno de los miembros de [1,2,3]
.
Por ejemplo:
/* .. tu código para inBetween e inArray */ sea arr = [1, 2, 3, 4, 5, 6, 7]; alerta (arr.filter (entre (3, 6))); // 3,4,5,6 alerta (arr.filter (inArray ([1, 2, 10]))); // 1,2
Abra una caja de arena con pruebas.
función entre(a, b) { función de retorno (x) { devolver x >= a && x <= b; }; } sea arr = [1, 2, 3, 4, 5, 6, 7]; alerta (arr.filter (entre (3, 6))); // 3,4,5,6
función en matriz (arr) { función de retorno (x) { return arr.includes(x); }; } sea arr = [1, 2, 3, 4, 5, 6, 7]; alerta (arr.filter (inArray ([1, 2, 10]))); // 1,2
Abra la solución con pruebas en un sandbox.
importancia: 5
Tenemos una variedad de objetos para ordenar:
dejar usuarios = [ { nombre: "John", edad: 20, apellido: "Johnson" }, { nombre: "Pete", edad: 18, apellido: "Peterson" }, { nombre: "Ann", edad: 19, apellido: "Hathaway" } ];
La forma habitual de hacerlo sería:
// por nombre (Ann, John, Pete) usuarios.sort((a, b) => a.nombre > b.nombre ? 1 : -1); // por edad (Pete, Ann, John) usuarios.sort((a, b) => a.edad > b.edad ? 1 : -1);
¿Podemos hacerlo aún menos detallado, así?
usuarios.sort(byField('nombre')); usuarios.sort(byField('edad'));
Entonces, en lugar de escribir una función, simplemente escriba byField(fieldName)
.
Escriba la función byField
que pueda usarse para eso.
Abra una caja de arena con pruebas.
función por campo (nombre del campo) { return (a, b) => a[nombre del campo] > b[nombre del campo] ? 1: -1; }
Abra la solución con pruebas en un sandbox.
importancia: 5
El siguiente código crea una serie de shooters
.
Cada función está destinada a generar su número. Pero algo anda mal…
función crearArmado() { dejar tiradores = []; sea yo = 0; mientras (yo < 10) { let shooter = function() { // crea una función de tirador, alerta (yo); // eso debería mostrar su número }; tiradores.push(tirador); // y lo agregamos a la matriz yo ++; } // ...y devolver el conjunto de tiradores tiradores de retorno; } let ejército = makeArmy(); // todos los tiradores muestran 10 en lugar de sus números 0, 1, 2, 3... ejército[0](); // 10 del tirador número 0 ejército[1](); // 10 del tirador número 1 ejército[2](); // 10 ...y así sucesivamente.
¿Por qué todos los tiradores muestran el mismo valor?
Corrige el código para que funcione según lo previsto.
Abra una caja de arena con pruebas.
Examinemos qué sucede exactamente dentro de makeArmy
y la solución será obvia.
Crea una matriz vacía shooters
:
dejar tiradores = [];
Lo llena con funciones a través de shooters.push(function)
en el bucle.
Cada elemento es una función, por lo que la matriz resultante se ve así:
tiradores = [ función () { alerta(i); }, función () { alerta(i); }, función () { alerta(i); }, función () { alerta(i); }, función () { alerta(i); }, función () { alerta(i); }, función () { alerta(i); }, función () { alerta(i); }, función () { alerta(i); }, función () { alerta(i); } ];
La matriz es devuelta por la función.
Luego, más tarde, la llamada a cualquier miembro, por ejemplo, army[5]()
obtendrá el elemento army[5]
de la matriz (que es una función) y lo llamará.
Ahora bien, ¿por qué todas estas funciones muestran el mismo valor, 10
?
Esto se debe a que no hay una variable local i
dentro de las funciones shooter
. Cuando se llama a una función de este tipo, se toma i
de su entorno léxico externo.
Entonces ¿cuál será el valor de i
?
Si miramos la fuente:
función crearArmado() { ... sea yo = 0; mientras (yo < 10) { let shooter = function() { // función del tirador alerta (yo); // debería mostrar su número }; tiradores.push(tirador); //agregar función a la matriz yo ++; } ... }
Podemos ver que todas las funciones shooter
se crean en el entorno léxico de la función makeArmy()
. Pero cuando se llama army[5]()
, makeArmy
ya ha terminado su trabajo y el valor final de i
es 10
( while
se detiene en i=10
).
Como resultado, todas las funciones shooter
obtienen el mismo valor del entorno léxico externo y es decir, el último valor, i=10
.
Como puede ver arriba, en cada iteración de un bloque while {...}
, se crea un nuevo entorno léxico. Entonces, para solucionar este problema, podemos copiar el valor de i
en una variable dentro del bloque while {...}
, así:
función crearArmado() { dejar tiradores = []; sea yo = 0; mientras (yo < 10) { sea j = i; let shooter = function() { // función del tirador alerta( j ); // debería mostrar su número }; tiradores.push(tirador); yo ++; } tiradores de retorno; } let ejército = makeArmy(); // Ahora el código funciona correctamente ejército[0](); // 0 ejército[5](); // 5
Aquí let j = i
declara una variable j
“iteración-local” y copia i
en ella. Las primitivas se copian "por valor", por lo que en realidad obtenemos una copia independiente de i
, que pertenece a la iteración del bucle actual.
Los tiradores funcionan correctamente, porque el valor de i
ahora vive un poco más cerca. No en el entorno léxico makeArmy()
, sino en el entorno léxico que corresponde a la iteración del bucle actual:
Este problema también podría evitarse si usáramos for
al principio, así:
función crearArmado() { dejar tiradores = []; para(sea i = 0; i < 10; i++) { let shooter = function() { // función del tirador alerta (yo); // debería mostrar su número }; tiradores.push(tirador); } tiradores de retorno; } let ejército = makeArmy(); ejército[0](); // 0 ejército[5](); // 5
Eso es esencialmente lo mismo, porque for
cada iteración se genera un nuevo entorno léxico, con su propia variable i
. Entonces, shooter
generado en cada iteración hace referencia a su propio i
, de esa misma iteración.
Ahora, como has puesto tanto esfuerzo en leer esto, y la receta final es tan simple (solo úsala for
), te preguntarás: ¿valió la pena?
Bueno, si pudieras responder fácilmente a la pregunta, no leerías la solución. Espero que esta tarea te haya ayudado a comprender un poco mejor las cosas.
Además, hay casos en los que se prefiere while
a for
y otros escenarios en los que estos problemas son reales.
Abra la solución con pruebas en un sandbox.