Comprende JS: Closures

Bruno Pineda
sngular-devs
Published in
8 min readOct 13, 2018

Parte VIII de la serie Comprende JS

Bueno, por fin hemos llegado a esta parte tan confusa o poco tangible que tiene Javascript como característica y que es parte aguas en muchas entrevistas de trabajo entre ser Junior o Senior y estamos hablando de los…

Closures

Vamos a tratar de ser lo mas claros posibles para comprender lo que esto significa, por lo cual no me gustaría iniciar con el aburrido y confuso lado conceptual de esto, sino empezar a explicar con ejemplos de una manera mas abstracta, comencemos.

Considera el siguiente ejemplo:

La forma de generar un closure es que una función se declare dentro de otra y esta ultima sea ejecutada welcome('bruce');, hasta este momento es cuando se activa dicha característica de Javascript que esta ahí para nosotros, pero vamos a retroceder un poco en nuestra serie para entender que hace un closure, vamos a voltear a mirar los primeros capítulos donde explicamos el contexto de ejecución y la pila de ejecución en si.

Recordemos que cada que se ejecuta o se invoca una función se genera un nuevo contexto de ejecución y este se apila en la execution stack o pila de ejecución, véase la siguiente imagen.

Execution stack

Como podemos ver en la imagen se genera el contexto de ejecución global y después al invocar la función welcome('bruce'), se crea su contexto y se apila en la execution stack con su lexical environment, el cual contiene las definiciones de variables, cuando termina de ejecutarse una función en este caso welcome(), su contexto de ejecución muere y sale de la pila de ejecución…

Esto sucedería sin la activación del closure, todo el contexto se va por completo de la pila para continuar con el siguiente contexto en la fila, que es en este caso el “global execution context”, pero como en este caso hay una función definida dentro de otra, entonces se activa el llamado closure y en vez de ocurrir lo anterior, sucede lo siguiente…

Entonces en algún lugar en memoria queda vivo el “lexical environment” con las definiciones de la función welcome(), ahora tomando de nuevo en cuenta el código que estamos explorando viene lo siguiente…

Zoom del código anterior

Cuando ejecutamos entonces, la función retornada por welcome(), en este caso la asignada a la variable greet, es decir, greet(), entonces se crea su contexto de ejecución en la pila…

Luego, inicia el proceso del “scope chain”, el cual ya vimos anteriormente en este articulo de la serie, donde en la fase de ejecución de greet(), se inicia la búsqueda de la referencia de id, al no encontrarla se inicia el “scope chain” y se empieza la búsqueda fuera del scope de greet(), es decir en su “outer environment”, pero es aquí donde sucede la “magia” del closure, que gracias a esta característica es que el outer environment de greet(), quedo vivo y encuentra la definición de id en este lugar, a esto amigos míos, se le llama…

Magic

Veamos otro ejemplo mas avanzado de closure, para ello considera el siguiente código…

Que esperarías que imprimiera las lineas 18, 19 y 20, tal vez 0, 1, 2, si esa fue tu respuesta es errónea, analicemos por fragmentos el código arriba descrito.

Primero se ejecuta la función getFunctions(), lo cual genera un nuevo contexto de ejecución en la pila con su Lexical environment y definiciones…

Donde vemos que esta declarada una variable i=0, al iniciar el for, entonces la fase de ejecución continua leyendo linea por linea, al ingresar al loop for, en cada iteracion lo que hace es guardar en un Array, la definición de una función.

Nota: Aquí se activa el closure, recordando que es debido a que existe una declaración de una función dentro de otra.

Una vez activada la característica closure y habiendo terminado de ejecutarse nuestra función getFunctions(), sabemos que hizo algo como esto…

El contexto de getFunctions(), se fue de la pila, pero su Lexical environment junto con sus definiciones quedaron vivas en memoria, gracias a que el closure se encuentra activo, después continua nuestra fase de ejecución del global context y de acuerdo al código continuamos con esto…

Después de la ejecución y asignación del resultado de getFunctions(), a la variable fs, vamos a ejecutar cada función almacenada en ese arreglo, entonces al ejecutar el primer item de dicho arreglo, se crea un contexto de ejecución para esa función en la pila…

Como en el ejemplo anterior en este también se inicia con el “scope chain” al no encontrar la definición de i, dentro del “scope” de fs[0](), entonces busca en el outer environment que gracias al closure activado es que esta vivo, recordando que ese outer environment es el lexical environment de getFunctions(), que quedo vivo en memoria pero hay una diferencia y es que en este caso antes de que el contexto de ejecución de getFunctions(), saliera de la pila, recordemos que hizo todas las iteraciones del for, por lo que antes de irse, dejo alterado el valor de i, dejando como valor el ultimo de la iteracion que en este caso es 3, entonces cuando el “scope chain” encuentra en el outer environment a i, se encuentra con que i=3

Cuando sucede termina de ejecutarse fs[0](), su contexto se va de la pila y después se continua con el global context y se ejecuta fs[1](), creando otro contexto de ejecución en la pila…

Y sucede exactamente lo mismo que el caso anterior “scope chain”…

Pero i, sigue teniendo el mismo valor porque asi lo “heredo” getFunctions(), en memoria, es decir que el resultado en todas las ejecuciones sera 3, 3, 3.

La confusión en la mayoría de los developers esta en que perdemos de vista que en este paso…

Solo estamos declarando y guardando la función, mas no ejecutando, lo cual hace la diferencia, ya que dentro de esa función esta una referencia apuntando a i, mas no se esta evaluando su valor aún, solo hasta que es ejecutada esta función es cuando se evalúa el valor de i.

Solución

Para solucionar este problema necesitaríamos crear otro contexto y declarar otra variable con el valor primitivo de i, en cada iteracion, ya que recordemos que los valores primitivos son únicos y se guardan por separado en memoria al ser asignados a otra variable, veamos…

Vamos de nuevo por pasos, primero tenemos la función getFunctions(), que al ejecutarse genera un nuevo contexto de ejecución con sus definiciones.

En la fase de ejecución de este contexto, cuando entramos al for, y hacemos el push de una función, a diferencia del caso anterior esta vez lo agregamos a través de una IIFE, si… recuerdan que una IIFE cuando es ejecutada genera o se encapsula en un scope y contexto distinto pudiendo tener sus propias definiciones en memoria, veamos un zoom de la IIFE…

Si observamos, la expresión de función que se encuentra adentro del grouping que engloba la IIFE, recibe como parámetro nuestro valor primitivo en ese momento de i, a diferencia del caso anterior, aquí si estamos ya evaluando el valor de i, y se lo estamos pasando a nuestra variable local del contexto de la IIFE llamada j, que es finalmente la que utilizamos para hacer el console.log(), entonces que sucedió detrás de esto, bueno primero ya sabemos que genero el contexto de getFunctions(), después justo antes de hacer el push al arreglo, ejecutamos nuestra IIFE para así evaluar el valor de i y asignárselo a nuestra variable local de esa IIFE con un valor primitivo lo cual hace que cuando muera el contexto de ejecución de la IIFE, j, se quede con ese valor en memoria y gracias al closure permanezca vivo el Lexical environment con esa definición y valor de j, por pasos y gráficamente…

Se crea el contexto con definiciones justo antes de hacer el push, ya que, es cuando se ejecuta nuestra IIFE y es cuando se le da a través del parámetro j un valor para conservar en su Lexical environment…

Después cuando termina de ejecutarse el contexto de IIFE, sale de la pila de ejecución pero como dentro de IIFE se declara una función entonces se activa el closure, y sucede lo que ya sabemos, el contexto muere pero deja su legado es decir su Lexical environment con sus definiciones en este caso j=0, el cual se convertirá en el outer environment del función que se guardo en ese momento en el arreglo cuando sea ejecutada en el futuro, si pudiéramos ver como queda en memoria cada uno de los items del arreglo después de las iteraciones lo que dejo cada IIFE ejecutada veríamos algo parecido a esto…

Teniendo esto presente, podemos decir que cuando se ejecute cada una de las funciones que están dentro del arreglo, se creara en cada ejecución su contexto futuro que esta guardado en memoria, mas o menos así…

Esto es lo mismo por cada ejecución

Te hace sentido?, no hay magia es simplemente una característica que tiene Javascript es todo.

Conclusión

Es verdad que al principio puede parecer muy confuso, pero solo es cuestión de practicarlo y entenderlo, cuando lo haces, lo puedes imaginar sin tanta complejidad y explicarlo, es entonces cuando tienes en tus manos una poderosa característica para sacar provecho en Javascript.

<< episodio anterior || episodio siguiente >>

--

--