Closures en JavaScript: entiéndelos de una vez por todas

3. December 2012 07:35 by Oscar.SS in Desarrollo Web  //  Tags:   //   Comments (0)

Introducción

Los closures, en castellano cierres o clausuras, son una potente característica de JavaScript que también podemos encontrar en otros lenguajes como C#, Objective-C, Eiffel y otros. Este concepto que tradicionalmente pertenece a la programación funcional tiene su origen en el lenguaje de programación Scheme.

En este artículo vamos a comenzar por identificar en el código cuando se producen los closures para luego ver que tienen de especial. Después pasaremos a ver algunos comportamientos que nos servirán para terminar completamente de entender cómo funcionan. Para finalizar veremos un par de ejemplos de aplicaciones prácticas.

 

Concepto

Cuando nos acercamos por primera vez al concepto de closure es habitual encontrarnos con problemas a la hora de entenderlo, a pesar de que, seguramente en muchas ocasiones los habremos utilizado sin saberlo. Por este motivo, antes de intentar dar una definición más o menos académica, creo que es mucho más práctico saber identificar los closures en el código, que al fin y al cabo es el lenguaje natural del programador.

 

Identifiquemos los elementos que se producen en el código anterior.

  • En primer lugar podemos ver claramente que tenemos dos funciones y que la función greet() está definida dentro de la función personalizedGreet(). 
  • En segundo lugar podemos observar como la función greet() utiliza en su cuerpo una variable local perteneciente al ámbito de la función personalizedGreet().

Siempre que se produzcan los dos elementos anteriores podemos decir que tenemos un closure entre manos. ¡Así de sencillo!. Comprobemos ahora como funciona el código que hemos visto en la imagen anterior.

        //En otra parte del código...

        
var sayHello personalizedGreet("Óscar")/* Se almacena en la variable sayHello el código: 
                                                      
                                                      function greet() 
                                                      { 
                                                           return hello + name; 
                                                      }
                                                   */

        
alert(sayHello());  // Se ejecuta el código anterior y se muestra en pantalla: Hello Óscar

Debemos comentar aquí que la apariencia más habitual de los closures es cuando utilizamos funciones anónimas como en el siguiente ejemplo.

        function whatAreYouWritingAbout()
        {
            
var subject "About Closures";

            var 
message = function () { return subject};

            return 
message;
        
}


        
//En otra parte del código...

        
var writingAbout whatAreYouWritingAbout();

        alert
(writingAbout())//Muestra en pantalla: About Closures

Acabamos de ver como identificar en el código cuando se producen los closures pero...¿qué tienen de particular?, ¿qué hace tan especial al código anterior?.

Analicemos paso a paso que sucede supuestamente cuando se ejecuta la función whatAreYouWritingAbout():

  1. Comienza la ejecución de la función whatAreYouWritingAbout().
  2. Se guarda en memoria una variable con el valor string "About Closures".
  3. Se guarda en memoria una variable con una referencia al código: function () { return subject; }.
  4. La función whatAreYouWritingAbout() devuelve el código anterior.
  5. Fin de la ejecución de la función whatAreYouWritingAbout(). 
  6. Se llama al Garbage Collector para que elimine la variable subject dado que la función ha terminado y no la necesitará hasta su próxima ejecución donde será otra vez definida y asignada.

Según lo expuesto anteriormente cuando ejecutamos las líneas de código siguientes.

        var writingAbout whatAreYouWritingAbout();

        alert
(writingAbout())//Muestra en pantalla: About Closures

Literalmente es como si estuviéramos escribiendo lo siguiente.

        var writingAbout = function () { return subject};

        alert
(writingAbout())//Muestra en pantalla: About Closures

Pero evidentemente, en este contexto o ámbito del código, la variable subject no existe (o está fuera de ámbito) y además su valor supuestamente fue destruido al salir de la función whatAreYouWritingAbout(). Por lo tanto, cuando se ejecuta la función writingAbout() debería dar un error tipo Uncaught ReferenceError: subject is not defined. Sin embargo, el código muestra el mensaje correctamente...¿Por qué?.

Cuando JavaScript encuentra en el código un closure le indica al Garbage Collector que no destruya las variables (están quedan guardadas o encerradas) que la función "interna" necesita para su correcta ejecución. Por lo tanto, a pesar de que las variables que esta función utiliza se encuentran en otro ámbito en el momento de su ejecución, JavaScript guardó una referencia al valor de las mismas y por lo tanto siempre están accesibles para la función. Podríamos decir que este es el secreto o la magia de los closures.

 

Comportamiento

Para terminar de captar el concepto veamos ahora una serie de comportamientos o características que tienen los closures y que tendremos que tener siempre presentes cuando trabajamos con esta potente herramienta del lenguaje JavaScript.

Como se crean los closures

Cuando el intérprete de JavaScript encuentra un closure guarda las variables locales que las funciones internas van a necesitar. Esto ya lo hemos comentado pero...¿cómo acceden las funciones a estas variables?. Veamos algunos apuntes sobre esto:

  • Cuando una variable local se almacena en memoria, si esta cambia a lo largo de la función externa, en memoria se almacenará el último valor asignado al salir de la función externa.
  • Cada función interna guarda una referencia a la posición de memoria donde se almacena la variable pudiendo acceder a su valor.

La idea principal es que el valor de la variable puede ser modificada a lo largo del código y que las funciones internas guardan una referencia a la memoria donde se encuentra el último valor de la variable que fue asignada al salir de la función externa. Es importante tener esto presente y como veremos ahora tiene algunos comportamientos que en principio podrían sorprender.
 
 

Definir o modificar la variable después de la función interna

En los ejemplos de código que hemos comentado anteriormente la variable local de la función externa se encontraba definida antes que la función anidada. Lo contrario es perfectamente válido.

        function personalizedGreet(name)
        {
            
var greet = function () { return hello + name};

            var 
hello "Hello ";  // Variable local definida después de la función anidada

            
return greet;
        
}


        
//En otra parte del código...

        
var greeting personalizedGreet("Oscar");

        alert
(greeting())//Muestra en pantalla: Hello Oscar

Como podemos observar no tiene ninguna influencia cuando es definida la variable local, la función interna guarda una referencia a la posición de memoria y utiliza el valor de la variable cuando la necesita.

Por cierto, en este ejemplo la función interna anónima utiliza realmente dos variables locales, hello y name. Los parámetros de la función externa son considerados lógicamente como variables de este ámbito. Anteriormente se omitió este hecho para simplificar el concepto y explicación.

En el siguiente ejemplo tenemos una función que define en su interior una constante (con valor 10) que será incrementada una cantidad pasada como parámetro a la función.

        function constIncrease(amount)
        {
            
var ten 10;

            var 
codeFunc = function () { alert(ten)};

            
ten +amount;

            return 
codeFunc;
        
}


        
//En otra parte del código...

        
var execute constIncrease(5);

        
execute();

Pensar por un momento cual será la salida en pantalla de este código, 10 ó 15. Los que hayáis respondido 15 estáis en lo cierto. Los que no, es normal, se tiende a pensar que el closure queda establecido en el comento que se define la función anidada, pero no es así. Como ya hemos comentado, la función interna simplemente almacena una referencia al último valor de la variable establecido cuando la función externa termina de ejecutarse.

 

Cada llamada es un closure distinto

Visto lo anterior podría pensarse que el closure es un elemento global que persiste entre llamadas. No es así, por cada llamada que hagamos a la función externa estaremos creando un nuevo e individual closure. Veamos el ejemplo siguiente.

        function personalizedGreet(name) {

            
var hello "Hello ";

            return function 
() { alert(hello + name)};
        
}

        
        
//Creamos dos closures distintos

        
var greet1 personalizedGreet("Pedro");
        var 
greet2 personalizedGreet("María");

        
greet1();  //Muestra en pantalla: Hello Pedro
        
greet2();  //Muestra en pantalla: Hello María

 

Cuando tenemos varias funciones internas

Veamos ahora un ejemplo en el que varias funciones internas guardan una referencia a la misma variable local, o podríamos decir también que mantienen una referencia al mismo closure. En este ejemplo la función externa no devuelve ningún objeto, vamos almacenar las funciones internas en variables globales.

        var gExecute1 = null;
        var 
gExecute2 = null;
        var 
gExecute3 = null;

        function 
createClosure() {
            
var num 10;
            
gExecute1 = function () { alert(num)};

            
num++;
            
gExecute2 = function () { alert(num)};

            
num "¡¡Toma ya!!!"
            
gExecute3 = function () { alert(num)};
        
}

        createClosure()
;
        
gExecute1();
        
gExecute2();
        
gExecute3();

Piense el lector por un momento cuál será la salida de este script. Efectivamente, la salida son tres mensajes seguidos con el texto "¡¡Toma ya!!". Notar que las tres funciones internas mantienen una referencia al mismo valor, es decir, las tres variables globales pertenecen al mismo closure. De hecho, este podría ser el caso del esquema que  hemos visto en la imagen cuando hablábamos de como se crean los closures. Pero compliquemos el código un poco.

        var gExecute1 = null;
        var 
gExecute2 = null;
        var 
gExecute3 = null;

        function 
createClosure()
        {
            
var num 10;
            
gExecute1 = function () { alert(num)};
 
            
gExecute2 = function () { alert(++num)};
 
            
gClosure3 = function () { alert(num "¡¡Toma ya!!!")};
        
}

        createClosure()
;
        
gExecute1();  // Ejecuta el código: function () { alert(num); }; --> Muestra en pantalla: 10
        
gExecute2();  // Ejecuta el código: function () { alert(++num); }; --> Muestra en pantalla: 11
        
gExecute3();  // Ejecuta el código: function () { alert(num = "¡¡Toma ya!!!"); }; --> Muestra en pantalla: ¡¡Toma ya!!!
        
        //¿Qué mostrará ahora la siguiente linea de código...?
        
gExecute1();

Si volvemos a ejecutar el código de la función gExecute1() obtenemos el mensaje "¡¡Toma ya!!". Esto es totalmente lógico porque esta función tiene una referencia al mismo closure que gExecute3() que fue la última función en modificar el closure que comparten.

 

Todo variable puede pertenecer al closure

Debemos tener en cuenta que todo tipo de variable que se declare dentro del ámbito de la función externa y sea utilizada por la función anidada pertenecerá al closure. Esto puede dar algunos resultados inesperados por creación de closures no previstos. Por ejemplo, cuando ejecutamos bucles debemos prestar atención si dentro de la función interna utilizamos la variable contador del bucle, esta es a todos los efectos una variable local de la función externa y por lo tanto pertenecerá al closure.

        function testLoop(length)
        {
            
var list = new Array();

            for 
(var 0i <lengthi++) {

                list[i] 
"Item" + i;
            
}

            
return function () {
                
alert("i = " + i
                       + 
"\n" +
                       
"list[" + i + "] = " + list[i]);
            
};
        
}

        
var execute testLoop(5);
        
execute();

        
/*
        ---Salida del programa---

          i = 6;
          list[6] = undefined

        */

Como podemos observar el contador también pertenece al closure y está disponible para la función interna fuera del ámbito de la función externa. Además, y aquí es cuando tenemos que tener cuidado, el último valor para esta variable es 6 porqué el bucle ejecuta una vez más la instrucción i++ para luego realizar la comprobación i <= length y salir del bucle.

 

Aplicaciones prácticas

Ahora que seguramente que ya hemos entendido como se comportan los closures pasemos a algo más práctico. En seguida vamos a ver algunos ejemplos donde podemos comprobar en el campo de batalla la potencia de esta característica del lenguaje. Tenga el lector en cuenta que aplicaciones sobre este concepto pueden haber tantas como las necesidades o imaginación de los programadores, aquí veremos solo algunas.

 

Ejecutar funciones retardadas

Quizás este sea el ejemplo práctico más clásico y que el lector seguramente ya conocerá. En ocasiones en nuestro código JavaScript podemos necesitar realizar operaciones con retardo, para ello JavaScript nos provee de las funciones setTimeout() y setInterval().

Normalmente llamaremos a estas funciones pasándole como parámetros el nombre de la función a ejecutar y el tiempo de retardo en milisegundos. El problema a resolver es que sucede si la función que queremos ejecutar con retardo toma uno o más parámetros. En principio no tenemos forma de pasarle los parámetros a nuestra función dado que setTimeout() y setInterval() solo admiten el nombre de la función como primer parámetro. Esto es fácil de resolver con un closure.

        function additionNumber(a, b)
        {
            
return function () { alert(a + " + " + b + " = " + (a + b))};
        
}


        
var execute additionNumber(1040);

        
setTimeout(execute, 3000);  //Después de 3 segundos se muestra en pantalla: 10 + 40 = 50

 

Acceso desde miembros privados a miembros públicos

En un artículo anterior cuando hablamos de las funciones constructoras comentamos que desde los métodos privados no podíamos tener acceso a las propiedades públicas del objeto creado. Retomando el ejemplo de código que vimos en aquel artículo veamos como saltarnos este pequeño escollo.

        function Constructor(msjPrivado, msjPublico) {

            
var propiedadPrivada msjPrivado;
            this
.propiedadPublica msjPublico;

            var 
that = this; /*
                                La variable 'that' será guardada en el closure para ser 
                                utilizada en su momento por la función metodoPrivado()
                              */

            
var metodoPrivado = function () {
                
alert(propiedadPrivada);
                alert
(that.propiedadPublica);
            
};

            this
.metodoPublico = function () {
                 metodoPrivado()
;
            
};
        
}


        
var obj = new Constructor("mensaje privado""mensaje público");

        
obj.metodoPublico()/*
                                Muestra en pantalla dos mensajes seguidos:
                                mensaje privado
                                mensaje público
                            */

Personalizar eventos para objetos determinados del DOM

Supongamos que queremos encapsular en objetos del DOM la típica funcionalidad para cuando el ratón se posiciona sobre un elemento HTML y este cambie el texto de color y el estilo del puntero, cuando el ratón abandone el elemento, el texto y puntero vuelven a su estado anterior.

        //Asocia un objeto a un evento personalizado
        
function funcDelegate(obj, methodName) {
            
// {1}
            
return function (e) {
                e 
e || window.event;
                return 
obj[methodName](this, e)// {2}
            
};
        
}

        
//Crea un objeto y enlaza los eventos del DOM con los eventos personalizados
        
function DOMObject(id) {
            
this._element = document.getElementById(id);

            if 
(this._element) {
                
// {3}
                
this._element.onmouseover funcDelegate(this"customOnMouseOver")// {4}
                
this._element.onmouseout funcDelegate(this"customOnMouseOut");
            
}
        }

        
//Definimos el código de nuestros eventos personalizados
        
DOMObject.prototype.customOnMouseOver = function (obj, event) {
            obj.style.cursor 
"pointer";
            
obj.style.color "red";
        
};

        
DOMObject.prototype.customOnMouseOut = function (obj, event) {
            obj.style.cursor 
"pointer";
            
obj.style.color "black";
        
};

        
//Ahora podemos activar todo este mecanísmo para distintos elementos de nuestra página
        
var myDiv = new DOMObject("aDiv");
        var 
myButton = new DOMObject("aButton");
  1. Aquí es donde se produce el closure. Como se puede apreciar la función interna guardará una referencia a un objeto pasado como parámetro y al nombre de una función que en este caso será el nombre de nuestro evento personalizado. Nótese una vez más, aquí simplemente se está creando el closure y devolviendo un código, que será ejecutado más tarde, parecido al siguiente: objeto.eventoPersonalizado(elementoHTML, eventoDOM);. Aunque este código se ejecute después, la función seguirá teniendo acceso al objeto y al valor de la cadena que representa el nombre del evento.
  2. Aunque la palabra reservada this pueda parecer que se utiliza aquí, recordemos que en este momento solo se crea el closure y se devuelve un código que será ejecutado a posteriori. En el momento de ejecutarte este código, cuando un evento del DOM se dispare (ej: onclick), this hará referencia al elemento HTML que dispara el evento del DOM.
  3. En este momento asignamos a los eventos del DOM que nos interese el código devuelto por funcDelegate. Lo hemos mencionado antes, este código será del estilo: objeto.eventoPersonalizado(elementoHTML, eventoDOM);. Por lo tanto cuando se dispare un evento del DOM, como el onclick, estaremos llamando a una función que hace las veces de nuestro evento personalizado.
  4. Aquí this hace referencia al objeto creado con la función constructora DOMObject(id).

Seguramente el lector conozca otras formas más sencillas por medio de llamadas sucesivas a funciones de implementar el mismo comportamiento. La ventaja de hacerlo de esta forma es que estamos encapsulando la funcionalidad con los propios objetos del DOM, lo que nos proporciona más robustez y mantenibilidad del código y además nos permite disparar los eventos personalizados cuando lo necesitemos en nuestro código.

        var myDiv = new DOMObject("aDiv");

        
myDiv.customOnClick(myDiv._element, null);

 

Referencia

Recent Comments

Comment RSS

Month List