Las devoluciones de llamada, los manejadores de eventos, las funciones de orden superior pueden acceder a variables de ámbito externo gracias a los cierres. Los cierres son importantes en la programación funcional, y a menudo se preguntan durante la entrevista de codificación de JavaScript.
Aunque se utilizan en todas partes, los cierres son difíciles de entender. Si no has tenido tu momento «¡Ahá!» para entender los cierres, entonces este post es para ti.
Empezaré con los términos fundamentales: alcance y alcance léxico. Luego, después de entender lo básico, sólo necesitarás un paso para entender finalmente los cierres.
Antes de empezar, te sugiero que resistas el impulso de saltarte las secciones de ámbito y ámbito léxico. Estos conceptos son cruciales para los cierres, y si los entiendes bien, la idea de cierre se hace evidente.
1. El ámbito
Cuando defines una variable, la quieres accesible dentro de unos límites. Por ejemplo, una variable result
tiene sentido que exista dentro de una función calculate()
, como detalle interno. Fuera del calculate()
, la variable result
es inútil.
La accesibilidad de las variables se gestiona por alcance. Usted es libre de acceder a la variable definida dentro de su ámbito. Pero fuera de ese ámbito, la variable es inaccesible.
En JavaScript, un ámbito es creado por una función o bloque de código.
Veamos cómo el ámbito afecta a la disponibilidad de una variable count
. Esta variable pertenece a un ámbito creado por la función foo()
:
function foo() { // The function scope let count = 0; console.log(count); // logs 0}foo();console.log(count); // ReferenceError: count is not defined
count
se accede libremente dentro del ámbito de foo()
.
Sin embargo, fuera del ámbito de foo()
count
es inaccesible. Si intentas acceder a count
desde fuera de todos modos, JavaScript lanza ReferenceError: count is not defined
.
En JavaScript, el ámbito dice: si has definido una variable dentro de una función o bloque de código, entonces puedes usar esta variable sólo dentro de esa función o bloque de código. El ejemplo anterior demuestra este comportamiento.
Ahora, veamos una formulación general:
El ámbito es una política de espacio que rige la accesibilidad de las variables.
Surge una propiedad inmediata: el ámbito aísla las variables. Eso es genial porque diferentes ámbitos pueden tener variables con el mismo nombre.
Puedes reutilizar nombres de variables comunes (count
index
current
value
, etc) en diferentes ámbitos sin colisiones.
foo()
y bar()
los ámbitos de las funciones tienen sus propias variables, pero con el mismo nombre count
:
function foo() { // "foo" function scope let count = 0; console.log(count); // logs 0}function bar() { // "bar" function scope let count = 1; console.log(count); // logs 1}foo();bar();
count
las variables de los ámbitos de función foo()
y bar()
no colisionan.
2. Anidación de scopes
Juguemos un poco más con los scopes, y pongamos un scope dentro de otro.
La función innerFunc()
está anidada dentro de una función externa outerFunc()
.
¿Cómo interactuarían los 2 scopes de la función entre sí? Puedo acceder a la variable outerVar
de outerFunc()
desde dentro del ámbito innerFunc()
?
Probemos eso en el ejemplo:
function outerFunc() { // the outer scope let outerVar = 'I am outside!'; function innerFunc() { // the inner scope console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
De hecho, la variable outerVar
es accesible dentro del ámbito innerFunc()
. Las variables del ámbito exterior son accesibles dentro del ámbito interior.
Ahora ya sabes 2 cosas interesantes:
- Los ámbitos pueden anidarse
- Las variables del ámbito exterior son accesibles dentro del ámbito interior
3. El ámbito léxico
¿Cómo entiende JavaScript que outerVar
dentro de innerFunc()
corresponde a la variable outerVar
de outerFunc()
?
Es porque JavaScript implementa un mecanismo de alcance llamado lexical scoping (o static scoping). El ámbito léxico significa que la accesibilidad de las variables está determinada por la posición de las variables en el código fuente dentro de los ámbitos de anidamiento.
Simplificando, el scoping léxico significa que dentro del ámbito interior se puede acceder a las variables de sus ámbitos exteriores.
Se llama léxico (o estático) porque el motor determina (en el momento del lexing) el anidamiento de ámbitos simplemente mirando el código fuente de JavaScript, sin ejecutarlo.
Así es como el motor entiende el fragmento de código anterior:
- Veo que defines una función
outerFunc()
que tiene una variableouterVar
. Bien. - Dentro del
outerFunc()
, veo que defines una funcióninnerFunc()
. - Dentro del
innerFunc()
, puedo ver una variableouterVar
sin declarar. Como uso el alcance léxico, considero que la variableouterVar
dentro deinnerFunc()
es la misma variable queouterVar
deouterFunc()
.
La idea destilada del ámbito léxico:
El ámbito léxico está formado por ámbitos exteriores determinados estáticamente.
Por ejemplo:
const myGlobal = 0;function func() { const myVar = 1; console.log(myGlobal); // logs "0" function innerOfFunc() { const myInnerVar = 2; console.log(myVar, myGlobal); // logs "1 0" function innerOfInnerOfFunc() { console.log(myInnerVar, myVar, myGlobal); // logs "2 1 0" } innerOfInnerOfFunc(); } innerOfFunc();}func();
El ámbito léxico de innerOfInnerOfFunc()
está formado por los ámbitos de innerOfFunc()
func()
y el ámbito global (el más externo). Dentro de innerOfInnerOfFunc()
puedes acceder a las variables de ámbito léxico myInnerVar
myVar
y myGlobal
.
El ámbito léxico de innerFunc()
está formado por func()
y el ámbito global. Dentro de innerOfFunc()
puedes acceder a las variables de ámbito léxico myVar
y myGlobal
.
Por último, el ámbito léxico de func()
está formado únicamente por el ámbito global. Dentro de func()
puedes acceder a la variable de ámbito léxico .
4. El cierre
Bien, el ámbito léxico permite acceder a las variables estáticamente de los ámbitos exteriores. ¡Sólo queda un paso hasta el cierre!
Volvamos a ver el ejemplo de outerFunc()
y innerFunc()
:
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
Dentro del ámbito innerFunc()
se accede a la variable outerVar
desde el ámbito léxico. Eso ya se sabe.
Nota que la invocación de innerFunc()
ocurre dentro de su ámbito léxico (el ámbito de outerFunc()
).
Hagamos un cambio: innerFunc()
para que sea invocado fuera de su ámbito léxico (fuera de outerFunc()
). Podría innerFunc()
seguir accediendo a outerVar
?
Hagamos los ajustes en el fragmento de código:
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } return innerFunc;}const myInnerFunc = outerFunc();myInnerFunc();
Ahora innerFunc()
se ejecuta fuera de su ámbito léxico. Y lo que es importante:
innerFunc()
sigue teniendo acceso a outerVar
de su ámbito léxico, incluso siendo ejecutado fuera de su ámbito léxico.
En otras palabras, innerFunc()
cierra (es decir, captura, recuerda) la variable outerVar
de su ámbito léxico.
En otras palabras, innerFunc()
es un cierre porque cierra sobre la variable outerVar
de su ámbito léxico.
Has dado el último paso para entender qué es un cierre:
El cierre es una función que accede a su ámbito léxico incluso ejecutada fuera de su ámbito léxico.
Más sencillo, el closure es una función que recuerda las variables del lugar donde se define, independientemente de dónde se ejecute después.
Una regla general para identificar un closure: si ves en una función una variable ajena (no definida dentro de la función), lo más probable es que esa función sea un closure porque la variable ajena está capturada.
En el fragmento de código anterior, outerVar
es una variable ajena dentro del cierre innerFunc()
capturado del ámbito outerFunc()
.
Continuemos con ejemplos que demuestran por qué es útil el cierre.
5. Ejemplos de closure
5.1 Manejador de eventos
Veamos cuántas veces se hace clic en un botón:
let countClicked = 0;myButton.addEventListener('click', function handleClick() { countClicked++; myText.innerText = `You clicked ${countClicked} times`;});
Abre la demo y haz clic en el botón. El texto se actualiza para mostrar el número de clics.
Cuando se hace clic en el botón, handleClick()
se ejecuta en algún lugar dentro del código del DOM. La ejecución ocurre lejos del lugar de definición.
Pero al ser un cierre, handleClick()
captura countClicked
del ámbito léxico y lo actualiza cuando se produce un clic. Además, myText
también se captura.
5.2 Callbacks
Capturar variables del ámbito léxico es útil en los callbacks.
Un setTimeout()
callback:
const message = 'Hello, World!';setTimeout(function callback() { console.log(message); // logs "Hello, World!"}, 1000);
El callback()
es un cierre porque captura la variable message
.
Una función iteradora para forEach()
:
let countEven = 0;const items = ;items.forEach(function iterator(number) { if (number % 2 === 0) { countEven++; }});countEven; // => 2
El iterator
es un cierre porque captura la variable countEven
.
5.3 Programación funcional
Los cierres ocurren cuando una función devuelve otra función hasta que los argumentos son suministrados completamente.
Por ejemplo:
function multiply(a) { return function executeMultiply(b) { return a * b; }}const double = multiply(2);double(3); // => 6double(5); // => 10const triple = multiply(3);triple(4); // => 12
multiply
es una función currada que devuelve otra función.
La curación, un concepto importante de la programación funcional, también es posible gracias a los cierres.
executeMultiply(b)
es un cierre que captura a
de su ámbito léxico. Cuando se invoca el cierre, la variable capturada a
y el parámetro b
se utilizan para calcular a * b
.
6. Conclusión
El ámbito es lo que rige la accesibilidad de las variables en JavaScript. Puede haber un scope de función o de bloque.
El ámbito léxico permite que el ámbito de una función acceda estáticamente a las variables desde los ámbitos exteriores.
Por último, un cierre es una función que captura las variables de su ámbito léxico. En palabras sencillas, el cierre recuerda las variables del lugar donde se define, sin importar dónde se ejecute.
Los cierres capturan variables dentro de manejadores de eventos, callbacks. Se utilizan en la programación funcional. Es más, en una entrevista de trabajo de Frontend te pueden preguntar cómo funcionan los closures.
Todo desarrollador de JavaScript debe saber cómo funcionan los closures. Lidia con ello ⌐■_■.
¿Qué tal un reto? 7 Preguntas de la entrevista sobre cierres en JavaScript. Puedes responderlas?