Todo lo relacionado con el Software libre y el mundo de la Tecnología

Breaking

martes, 9 de abril de 2013

Closures en JavaScript Parte I

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:
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.

Fuente: http://www.wextensible.com 

No hay comentarios:

Publicar un comentario