Dan Abramov sobre cierres de JavaScript

Los cierres son difíciles para los programadores porque son una construcción "invisible".



Cuando usa un objeto, variable o función, lo hace a propósito. Piensas, "Aquí es donde necesito una variable", y la agregas a tu código. Los cierres, sin embargo, son otra cosa. Si bien la mayoría de los programadores están comenzando a aprender acerca de los cierres, estas personas ya los utilizan sin saberlo. Probablemente te pase lo mismo. Entonces, aprender los cierres se trata menos de aprender una nueva idea que de aprender a reconocer algo con lo que se ha encontrado muchas veces antes. En pocas palabras, el cierre es cuando una función accede a variables declaradas fuera de ella. Por ejemplo, el cierre está contenido en este código:















let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));


Tenga en cuenta que user => user.startsWith(query)es una función. Ella usa una variable query. Y esta variable se declara fuera de la función. Este es un cierre.



Puede omitir la lectura si lo desea. El resto de este material ve los cierres bajo una luz diferente. En lugar de hablar sobre lo que son los cierres, esta parte del artículo entrará en detalles sobre la detección de cierres. Esto es similar a cómo trabajaban los primeros programadores en la década de 1960.



Paso 1: las funciones pueden acceder a variables declaradas fuera de ellas



Para comprender los cierres, debe estar bastante familiarizado con las variables y funciones. En este ejemplo, estamos declarando una variable fooddentro de una función eat:



function eat() {
  let food = 'cheese';
  console.log(food + ' is good');
}
eat(); //    'cheese is good'


¿Qué pasa si desea poder cambiar el valor de una variable más tarde food, fuera de la función eat? Para hacer esto, podemos eliminar la variable en sí de la función foody moverla a un nivel superior:



let food = 'cheese'; //     
function eat() {
  console.log(food + ' is good');
}


Esto le permite cambiar la variable food"desde el exterior" cuando sea necesario:



eat(); //  'cheese is good'
food = 'pizza';
eat(); //  'pizza is good'
food = 'sushi';
eat(); //  'sushi is good'


En otras palabras, la variable foodya no es eatlocal a la función . Pero la función eat, a pesar de esto, no tiene problemas al trabajar con esta variable. Las funciones pueden acceder a variables declaradas fuera de ellas. Deténgase un rato y compruébelo usted mismo, asegúrese de que no tiene problemas con esta idea. Una vez que este pensamiento se haya asentado firmemente en su mente, continúe con el segundo paso.



Paso 2: colocar el código en la llamada a la función



Digamos que tenemos un código:



/*   */


No importa qué código sea. Pero digamos que necesitamos ejecutarlo dos veces.



La primera forma de hacer esto es simplemente hacer una copia del código:



/*   */
/*   */


Otra forma es poner el código en un bucle:



for (let i = 0; i < 2; i++) {
  /*   */
}


Y la tercera forma, que es especialmente interesante para nosotros hoy, es poner este código en una función:



function doTheThing() {
  /*   */
}
doTheThing();
doTheThing();


Usar una función nos da la máxima flexibilidad, ya que nos permite llamar al código dado cualquier cantidad de veces, en cualquier momento y desde cualquier lugar del programa.



De hecho, si es necesario, podemos limitarnos a una sola llamada de la nueva función:



function doTheThing() {
  /*   */
}
doTheThing();


Tenga en cuenta que el código anterior es equivalente al fragmento de código original:



/*   */


En otras palabras, si tomamos algún fragmento de código y lo "ajustamos" en una función, y luego llamamos a esta función exactamente una vez, entonces no influiremos de ninguna manera en lo que hace este código. Existen algunas excepciones a esta regla, a las que no prestaremos atención, pero, en general, podemos asumir que esta regla es cierta. Piénsalo un rato, acostúmbrate a esta idea.



Paso 3: detección de cierres



Descubrimos dos ideas:



  • Las funciones pueden trabajar con variables declaradas fuera de ellas.
  • Si coloca el código en una función y llama a esta función una vez, no afectará los resultados del código.


Ahora hablemos de lo que pasará si se combinan estas dos ideas.



Tomemos el código de muestra que vimos en el primer paso:



let food = 'cheese';
function eat() {
  console.log(food + ' is good');
}
eat();


Ahora pongamos todo este ejemplo en una función que planeamos llamar solo una vez:



function liveADay() {
  let food = 'cheese';
  function eat() {
    console.log(food + ' is good');
  }
  eat();
}
liveADay();


Lea los dos ejemplos de código anteriores y asegúrese de que sean equivalentes.



¡El segundo ejemplo funciona! Pero echémosle un vistazo más de cerca. Tenga en cuenta que la función eatestá dentro de una función liveADay. ¿Está esto permitido en JavaScript? ¿Es realmente posible envolver una función dentro de otra?



Hay lenguajes en los que el código estructurado de esta manera resultará incorrecto. Por ejemplo, en C, dicho código sería incorrecto (no hay cierres en este idioma). Esto significa que al usar C, nuestra segunda conclusión será incorrecta: no puede simplemente tomar un fragmento de código arbitrario y "envolverlo" en una función. Pero no existe tal limitación en JavaScript.



Pensemos nuevamente en este código, prestando especial atención a dónde se declara la variable y dónde se usa.food:



function liveADay() {
  let food = 'cheese'; //  `food`
  function eat() {
    console.log(food + ' is good'); //   `food`
  }
  eat();
}
liveADay();


Repasemos este código paso a paso juntos. Primero, declaramos, en el nivel superior, una función liveADay. La llamamos de inmediato. Esta función tiene una variable local food. La función también está declarada en él eat. Entonces la liveADayfunción se llama internamente eat. Dado que la función eatestá dentro de una función liveADay, eat"ve" todas las variables declaradas en liveADay. Es por eso que una función eatpuede leer el valor de una variable food.



A esto se le llama cierre.



Hablamos de la existencia de un cierre cuando una función (como eat) lee o escribe el valor de una variable (como food), que se declara fuera de ella (por ejemplo, en una función liveADay).



Piense en estas palabras, vuelva a leerlas. Ponte a prueba encontrando de lo que estamos hablando en nuestro código de muestra.



Aquí hay un ejemplo que se dio al principio del artículo:



let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));


Podría ser más fácil notar el cierre reescribiendo este ejemplo usando una expresión de función:



let users = ['Alice', 'Dan', 'Jessica'];
// 1.  query    
let query = 'A';
let user = users.filter(function(user) {
  // 2.     
  // 3.      query (    !)
  return user.startsWith(query);
});


Cuando una función accede a una variable declarada fuera de ella, la llamamos cierre. El término en sí se usa de manera bastante vaga. Algunas personas llamarán a la función anidada en sí, que se muestra en el ejemplo, "cierre". Otros pueden referirse a un descriptor de acceso variable externo llamándolo "cierre". En la práctica, esto no importa.



Función llamada fantasma



Los cierres pueden parecerle un concepto engañosamente simple ahora. Pero esto no significa que carezcan de algunas características no obvias. Si piensa detenidamente en el hecho de que una función puede leer y escribir los valores de variables fuera de ella, resulta que esto tiene consecuencias bastante graves.



Por ejemplo, esto significa que dichas variables “vivirán” siempre que se pueda llamar a una función anidada dentro de otra función.



function liveADay() {
  let food = 'cheese';
  function eat() {
    console.log(food + ' is good');
  }
  //  eat   
  setTimeout(eat, 5000);
}
liveADay();


En este ejemplo, foodes una variable local dentro de una llamada a función liveADay(). Solo quiero decidir que esta variable "desaparecerá" después de salir de la función y nunca volverá a perseguirnos como a un fantasma.



Pero en la función, liveADayle pedimos al navegador que llame a la función eatdespués de cinco segundos. Y esta función lee el valor de la variable food. Como resultado, resulta que el motor JavaScript necesita mantener viva la variable foodasociada con esta llamada liveADay()hasta que se llame a la función eat.



En este sentido, los cierres se pueden considerar como "fantasmas" de llamadas a funciones pasadas, o como "recuerdos" de tales llamadas. Aunque la ejecución de la funciónliveADay()terminó hace mucho tiempo, las variables declaradas en él deben continuar existiendo mientras eatse pueda llamar a la función anidada . Afortunadamente, JavaScript se encarga de estos mecanismos, por lo que no necesitamos hacer nada especial en estas situaciones.



¿Por qué los "cierres" se llaman así?



Quizás se pregunte por qué los cierres se llaman así. La razón de esto es principalmente histórica. Cualquiera que esté familiarizado con la jerga informática podría decir que una expresión como esta user => user.startsWith(query)tiene un "enlace abierto". En otras palabras, de esta expresión se desprende claramente qué es user(parámetro), pero cuando se ve de forma aislada, no queda claro qué es query. Cuando decimos que es, de hecho, queryuna variable declarada fuera de la función, estamos "cerrando" ese enlace abierto. En otras palabras, obtenemos un cierre.



Los cierres no se implementan en todos los lenguajes de programación. Por ejemplo, en algunos lenguajes, como C, las funciones anidadas no se pueden utilizar en absoluto. Como resultado, la función solo puede trabajar con sus variables locales o con variables globales. Sin embargo, nunca existe una situación en la que pueda acceder a las variables locales de la función principal. En realidad, esta es una limitación muy desagradable.



También hay lenguajes como Rust que implementan cierres. Pero usan una sintaxis diferente para describir cierres y funciones normales. Como resultado, si necesita leer el valor de una variable fuera de una función, entonces, usando Rust, debe usar una construcción especial. La razón de esto es que el uso de cierres puede requerir que los mecanismos internos del lenguaje mantengan las variables externas (llamadas el "entorno") incluso después de que se haya completado la llamada a la función. Esta carga adicional en el sistema es aceptable en JavaScript, pero puede, cuando se usa en lenguajes de nivel bastante bajo, causar problemas de rendimiento.



Ahora, espero que comprenda el concepto de cierres en JavaScript.



¿Tiene dificultades para comprender los conceptos de JavaScript?






All Articles