Wextensible

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:

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:

Ejemplo:

funcionA() =
funcionB() =
funcionC() =

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. Por ejemplo, la variable creada globalVar = "ABCDEF"; se creó en Global, como se comprueba ahora al actuar con este botón .

Con las herramientas de desarrollo de los navegadores podemos hacer un seguimiento de la ejecución del código. Con Developer Tools de Chrome ponemos un punto de interrupción antes de ejecutar var funcionC = function(){. La primera imagen en la siguiente serie es la captura de pantalla de la herramienta en ese momento:

El alcance de las variables
El alcance de las variablesImagen no disponible
La función funcionC no está definida aún (funcionC: undefined). El siguiente paso será construir esa función.

En ese momento se observa que tanto funcionA como funcionB ya fueron creadas, mientras que funcionC es undefined, es decir, no está definida. En la imagen 2 vemos que el paso siguiente se salta todo el interior de la función y pasa a la siguiente sentencia, pues cuando se está construyendo la función no se ejecuta el código interior sino que se construye el objeto con todo lo necesario para que posteriormente pueda ser llamada con funcionC(). Es entonces cuando se ejecuta su interior. Ahora, tras la construcción, vemos que funcionC ya está definida. Por ejemplo se observa que no tiene argumentos (arguments: null) y también nos muestra el function scope o alcance de la función. Para funcionC su alcance es Global: Window, pues es el contexto donde está incluida esta función. Window es el objeto global que contiene todo lo relativo a una página.

Si avanzamos a la imagen 3 veremos la ejecución de la sentencia valor = funcionC(); que llama a la función y es entonces cuando se ejecuta como decíamos antes. Es necesario poner otro punto de interrupción ahí para ver como se ejecuta ese interior. Vea el Call stack o Pila de llamadas, mostrando funcionC que es la llamada que acabamos de hacer. Si desde el interior de esta función fuésemos encadenando llamadas a otras funciones, aquí irían apareciendo esa pila de llamadas. Volviendo al momento de la parada en la ejecución vemos que es antes de que las variables locales hayan sido asignadas. Por lo tanto local y variable son undefined, tal como se observa en el Scope variables o Alcance de las variables para esta funcionC. Tiene dos alcances, Local y Global. Dentro de la función se resuelve la referencia a variable buscando primero en Local y luego en Global. Observe que el alcance local también tiene una referencia this que apunta a Window, que es el alcance Global.

En la imagen 4 ya hemos asignado los valores a las variables locales, como puede observarse local: "X" y variable: 3. Las variables de un alcance hijo puede acceder al alcance del padre pero no al revés. Desde Global no podemos acceder a las variables locales de funcionC, pues de hecho una vez ejecutada la función esas variables ya ni siquiera existen. Un sistema de recolección de basura (Garbage Collector) se encarga de ir limpiando zonas de memoria como estas variables locales que se vuelven inaccesibles al finalizar la ejecución de una función. Las únicas variables que permanecen son las del alcance Global y otras que se preservan como es el caso de los problemas Funarg que veremos más adelante.

En la imagen 5 ya hemos dotado a valor: 3, resultado de haber ejecutado funcionC() y haber retornado su variable local. Vea también en esta imagen como variable: 2, siendo ésta la perteneciente al alcance Global y que se modificó en un paso previo al ejecutar funcionB(). Esta variable es la global que nada tiene que ver con la variable local de funcionC.

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.

Ejemplo:

getMiVarBis =  
miVar =
getMiVarBis() =

El código que generó el ejemplo es el siguiente:

<div class="ejemplo-linea">
    <div><code>getMiVarBis</code> = <code id="mens-1" class="azul">&nbsp;</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:

Ejemplo:

unaVar =
unaFun =
portaFun =
valor =

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.


En los apartados anteriores sobre el alcance de las variables y el problema de Funarg no he mencionado el término closure en ningún momento. Y realmente estábamos hablando de closures. Esto quiere decir que un closure no es más que una característica del funcionamiento del lenguaje, como se verá con más detalle en el siguiente tema.