Introducción a los Clousures en Javascript
Los closures (clausuras) en JavaScript
En programación informática una clausura (closure) es una función o referencia a una función que se construye en un entorno y puede acceder a variables no locales, llamadas variables libres, incluso después de finalizada su ejecución. El término "closure" se deriva del inglés "close", pues se define también como una función que puede tener variables libres en un entorno que las blinda o cierra (that "closes" the function). No debe confundirse los closures con las funciones anónimas,
pues ésta es solo una función que no se asocia con un nombre, es decir,
con un identificador. Pero a veces se implementan los closures con
funciones anónimas y puede dar lugar a esa confusión.
Los closures tienen que ver con el contexto de ejecución y el alcance (scope)
de las funciones y variables. JavaScript es ejecutado en contextos de
ejecución. Cuando se llama a una función se ejecuta en un contexto y si
dentro de esta función se llama a otra se crea otro nuevo contexto para
esa función. Cuando se retorna una función también se devuelve el
contexto. Un contexto tiene muchas cosas, pero principalmente nos
interesa saber que también almacena el alcance (scope)
de la función. Con esto se establece las variables locales y no locales
con sus valores en el momento de la ejecución a las que accede la
función. Todo esto se almacena en ese contexto y por tanto si se retorna
una función también se está devolviendo ese contexto.
Hay muchos sitios donde buscar información sobre este tema. La especificación ECMA-262 publica el estándar del lenguaje de JavaScript, al que denomina ECMAScript. Las última edición es ECMA-262-5 (revisión 5.1). La anterior es ECMA-262-3,
pues la edición 4 no fue finalmente publicada. Estos documentos están
orientados a la implementación del lenguaje por los navegadores más que
al programador que lo va usar. Aunque conviene conocer este estándar
para ver las definiciones de los distintos elementos que componen este
lenguaje, es mejor buscar documentación sobre Closures en otros sitios:
- Clausura (closure) en Wikipedia, para tener una primera aproximación a esta técnica de programación. Además tiene enlaces a otras páginas sobre el tema.
- JavaScript Closures, de Richard Cornford. Entra en mayor detalle acerca del tema de los contextos de ejecución y el alcance o scope de las variables. Conviene leerlo para entender el funcionamiento y el motivo de la existencia del closure.
- Explaining JavaScript Scope And Closures, de Robert Nyman. Es más corto pero suficiente para saber de que estamos hablando.
- ECMA-262. JavaScript. The core. Closures, con una resumen sobre Closures. Con más detalle en ECMA-262-3 in detail. Chapter 6. Closures. Ambos documentos de Dimitry Shoshnikov.
En este primer tema haré un repaso sobre el alcance de las variables y también sobre el problema del Funarg. En el siguiente tema intentaré explicar cómo funciona un closure. Luego veremos que de hecho el efecto closure a veces aparece de forma accidental. En el siguiente expondré como usar los closures para lograr el ocultamiento y encapsulamiento de los módulos, exponiendo algunos ejemplos de patrones de diseño de JavaScript. Un ejemplo de uso es acerca de arreglar el problema del espacio global de variables para aplicarlo a este sitio Wextensible.com. Por último planteo un cargador de módulos que también aplicaré a este sitio.
El alcance de las variables en JavaScript
Como
para cualquier lenguaje de programación, es necesario entender el
alcance de las variables en JavaScript. En este ejemplo tenemos tres
funciones que devuelven el valor de una variable:
funcionA()
= 1
funcionB()
= 2
funcionC()
= 3
Para seguir las explicaciones pondremos antes el código que generó el ejemplo anterior:
<div class="ejemplo-linea">
<div><code>funcionA()</code> = <code id="scope-0" class="azul"></code></div>
<div><code>funcionB()</code> = <code id="scope-1" class="azul"></code></div>
<div><code>funcionC()</code> = <code id="scope-2" class="azul"></code></div>
</div>
<script>
var variable = 1;
var funcionA = function(){
return variable;
};
var valor = funcionA();
document.getElementById("scope-0").innerHTML = valor;
var funcionB = function(){
variable = 2;
globalVar = "ABCDEF";
return variable;
};
valor = funcionB();
document.getElementById("scope-1").innerHTML = valor;
var funcionC = function(){
var local = "X";
var variable = 3;
return variable;
};
valor = funcionC();
document.getElementById("scope-2").innerHTML = valor;
</script>
Todo el script que pongamos en una página se ejecuta en el alcance Global. Así cuando declaramos
var variable = 1
se almacena en ese alcance. La funcionA
retornará el valor 1, pues las funciones acceden a los alcances donde fueron creadas. En este caso funcionA
se creó en Global y ahí existe una variable
a la que funcionA
puede acceder.
Dentro de
funcionB
declaramos variable=2
, sin usar var
previo. En este caso funcionB
busca en el alcance Global una con el nombre variable
. Si la encuentra le pone el valor 2. Si no la encuentra la crea en el alcance Global. Por eso es importante entender como funciona var
. Y aunque en el alcance global no es necesario, como para la primera sentencia var variable = 1
que pudiéramos haber puesto variable = 1
con el mismo resultado, es aconsejable hacerlo para no olvidar que la declaración de una variable sin var
hace que se cree en el espacio global si no existía previamente.Upwards funarg problem: Devolviendo una función
Este apartado y el siguiente intentan exponer el Problema Funarg. Se trata de una cuestión con muchos años, por ejemplo, en el año 1970 Joel Moses escribía Why the FUNARG problem should be called the Enviroment problem (Porque el problema Funarg debería llamarse problema de entorno). El término funarg es la abreviatura de functional arguments refiriéndose a un problema cuando una función recibe a otra función como argumento. Sin embargo también se manifiesta cuando una función devuelve otra función y aún así se hablaba de éste como un problema Funarg, es decir, de argumentos. El primer caso se denominó Downwards funarg problem en el sentido de que el problema se originaba trayendo funciones "hacia abajo", mientras que el segundo se denominó Upwards funarg problem porqué se devolvía una función "hacia arriba". Bueno, son términos que más bien nos traen mayor confusión. Intentaré poner un ejemplo de cada clase, empezando por Upwards funarg problem cuando una función devuelve otra función.getMiVarBis
= function (){return miVar;}
miVar
= 1
getMiVarBis()
= 0
<div class="ejemplo-linea"> <div><code>getMiVarBis</code> = <code id="mens-1" class="azul"> </code></div> <div><code>miVar</code> = <code id="mens-2" class="azul"></code></div> <div><code>getMiVarBis()</code> = <code id="mens-3" class="azul"></code></div> </div> <script> //Esta función devuelve una función function funcionExterna(){ var miVar = 0; var getMiVar = function (){ return miVar; }; return getMiVar; } //Extraemos la función interna getMiVar var getMiVarBis = funcionExterna(); document.getElementById("mens-1").innerHTML = getMiVarBis; //Ahora getMiVarBis = function(){return miVar;} //Declaramos una variable global con el mismo nombre var miVar = 1; document.getElementById("mens-2").innerHTML = miVar; //Llamamos a getMiVarBis() ¿Devolverá 0 o 1? document.getElementById("mens-3").innerHTML = getMiVarBis(); </script>
Declaramos una
funcionExterna
con una variable local miVar
con el valor cero. Se devuelve una función interna getMiVar
que simplemente devuelve esa variable local. Luego procedemos a llamar a
la función externa que devuelve la función interna y que la asignamos a
getMiVarBis
. El código de la función es exactamente el
mismo que el de la función interna, como es de esperar y puede comprobar
en la primera línea del resultado: getMiVarBis = function(){return miVar;}
A continuación declaramos una variable global
var miVar = 1
con el mismo nombre. Luego llamamos a la función con getMiVarBis()
que debería devolvernos 1, pero en cambio nos devuelve 0. ¿Cómo? ¿qué? ¿cuándo?. En el contexto global y antes de llamar a getMiVarBis()
es como si tuviéramos esto:var getMiVarBis = function(){ return miVar; }; var miVar = 1;
Si sólo tuviésemos este código al ejecutar
getMiVarBis()
nos devolvería 1. Pero no en el caso anterior, donde se ha preservado el contexto y alcance de la función interna. De esa forma, aunque el código sea function(){return miVar;}
, este miVar
quedó "congelado" en el alcance de la función interna como una variable libre. Y ahí miVar
tenía el valor 0, valor que permanecerá inmutable e inaccesible desde el exterior.
El problema reside en la llamada a la función externa con
var getMiVarBis = funcionExterna()
.
En los lenguajes de programación como JavaScript, una ejecución de una
función cualquiera supone que al finalizar esa ejecución todas las
variables y referencias internas serán eliminadas. pues ya no se tiene
acceso a ellas externamente y no tiene utilidad almacenar esos datos.
Pero al devolver una función todo eso no se pueden eliminar, pues la
función devuelta puede ser usada más adelante y es posible que tenga a
su vez referencias a elementos del contexto donde fue creada, como
sucede con este ejemplo con el contexto de funcionExterna()
. El problema Funarg reside en el alcance de la variable miVar
, una variable libre para la función que es devuelta. Cuando ésta función se ejecute ¿Debe usar miVar
del alcance donde se ejecutó? ¿O debe usar miVar
del alcance donde se construyó?
Algunos lenguajes de programación optaron por impedir que una función
pudiera devolver otra función, cortando de raíz el problema. Otros
lenguajes posibilitan un alcance dinámico, con lo que el programador puede señalar si quiere usar cualquier variable llamada miVar
que se encuentre en el contexto donde la función es ejecutada o, en
cambio, usar la del contexto de creación. Pero JavaScript optó sólo por
éste último, teniendo por tanto un caracter estático, blindando esas variables libres y haciendo siempre referencia a las mismas en cualquier contexto donde se ejecute la función.Downwards funarg problem: Pasando funciones en argumentos
El segundo caso de Funarg se denomina Downwards funarg problem y sucede cuando pasamos una función como argumento de otra. Veámos el ejemplo:
unaVar
= 1
unaFun
= function (){return unaVar;}
portaFun
= function portaFun(funarg){var unaVar=0;return funarg();}
valor
= 1
Antes de comentarlo ponemos el código que ejecuta este ejemplo:
<div class="ejemplo-linea"> <div><code>unaVar</code> = <code id="mens-4" class="azul"></code></div> <div><code>unaFun</code> = <code id="mens-5" class="azul"></code></div> <div><code>portaFun</code> = <code id="mens-6" class="azul"></code></div> <div><code>valor</code> = <code id="mens-7" class="azul"></code></div> </div> <script> //Una variable en el contexto global var unaVar = 1; document.getElementById("mens-4").innerHTML = unaVar; //Una función construida en contexto global var unaFun =function (){ return unaVar; }; document.getElementById("mens-5").innerHTML = unaFun; //Una función que porta otra como argumento function portaFun(funarg){ var unaVar = 0; return funarg(); } document.getElementById("mens-6").innerHTML = portaFun; //Llamamos a portaFun con la función constuida //en el contexto global. ¿valor = 0? ¿valor = 1? var valor = portaFun(unaFun); document.getElementById("mens-7").innerHTML = valor; </script>
En primer lugar declaramos la variable
unaVar
en el contexto global. A continuación se construye una función unaFun
en ese contexto que devuelve una variable libre con nombre unaVar
. Luego se construye otra función portaFun
que pasa un argumento de tipo función. Dentro del contexto de esta función se declara otra variable con el mismo nombre unaVar
. Al anteponer var
sucede que esta variable es de ese contexto y no del contexto global exterior. La función del argumento funarg
será cualquier función tal que al ejecutarla con funarg()
devuelva algo, devolución que a su vez será devuelta por portaFun
.
Llamando a
portaFun(unaFun)
sería de esperar que devolviera 0, pues unaFun
se va ejecutar en el contexto de portaFun
y ahí la varible unaVar
vale 0. Pues no, devuelve 1. Esto es porque la función unaFun
se construyó en el contexto global y se almacenó su alcance, en este caso usando la global unaVar = 1
. A partir de ahí se preservan las variables libres de unaFun
, variables que quedan "congeladas" sea cual sea el contexto posterior donde se vuelva a ejecutar esa función.
No hay comentarios:
Publicar un comentario