Wextensible

Closures accidentales: Manejando eventos en un bucle

A veces los closures pueden servir para conseguir ventajas. Pero otras veces este efecto puede producir resultados inesperados. Es el caso cuando declaramos funciones dentro de un bucle, como una función anónima para manejar un evento, donde las variables locales del bucle están en un contexto padre en relación con las nuevas funciones creadas dentro del bucle. Serán entonces variables libres para las funciones que manejarán el evento. Y esto, que en otros casos es una ventaja, ahora puede ser un problema. Veámos esto en un ejemplo:

Ejemplo:

 
Al hacer click en cualquier elemento el valor de i no es el esperado:
 

El código es el siguiente:

<div class="ejemplo-linea" id="ejlin">
   <input type="button" value="Crear 3 div" onclick="construirHtml()" />
   <div id="div-div" style="border: gray solid 1px; padding: 0.2em">&nbsp;</div>
   Al hacer click en cualquier elemento el valor de <code>i</code> no es el esperado:
   <pre id="mensaje-click" style="border: gray solid 1px">&nbsp;</pre>
</div>
<script>
    function construirHtml(){
        var divDiv = document.getElementById("div-div");
        divDiv.innerHTML = "";
        document.getElementById("mensaje-click").innerHTML = "&nbsp;";
        for (var i=0, elemento; i<3; i++){
            elemento = document.createElement("div");
            elemento.title = i;
            elemento.style.border = "blue solid 1px";
            elemento.style.display = "inline block";
            elemento.innerHTML = "Elemento " + i;
            elemento.onclick = function(){
                document.getElementById("mensaje-click").innerHTML = 'ONCLICK elemento ' +
                '<span class="resalte-amarillo">i = ' + i + '</span>' +
                " (Title = " + this.title + ")";
            };
            divDiv.appendChild(elemento);
        }
    }
</script>

Al pulsar el botón llama a la función construirHtml(). Hay un bucle con tres iteraciones para construir otros tantos elementos <div>. El número de iteración i se incluye en el elemento y en su atributo title. Además asignamos al evento onclick una función anónima para manejar ese evento. Se trata de que cuando pulsemos sobre el elemento nos de un mensaje con el número de iteración i del bucle y también que extraiga su atributo title.

Construimos los elementos con el botón y luego probamos a pulsar sobre cada elemento. Esperaríamos que cada elemento tuviese su correspondiente valor de i. Pero ese i será 3 para todos. Vea que i=3 es el valor que tomó el bucle antes de salir, pues realmente iteró por 0,1,2 y cuando tomó el valor 3 finalizó el bucle.

Este efecto que puede parecer inesperado no lo es tanto si entendemos el contexto de las funciones que estamos creando. De hecho ahí se está produciendo un efecto de closure no deseado. O closures accidentales si lo traducimos literalmente del término accidental closures como suele verse en inglés. Se observa que i es una variable libre para las tres funciones anónimas que manejarán el evento. Esas tres funciones se devuelven a un elemento HTML. Por lo tanto cuando finaliza la ejecución de la función construirHtml() JavaScript no puede eliminar las referencias a la variable libre i, creándose por tanto un closure. Las tres funciones compartirán la misma variable y cuando se ejecuten harán referencia a la misma i mostrando el valor 3 que fue el valor con el que finalizó en la ejecución de la función construirHtml(). El hecho de que compartan una misma variable libre puede ser útil en otros casos, pero no en éste ejemplo.

Este problema se puede arreglar haciendo uso de closures, lo cual resulta un poco paradójico pues el problema lo originó un closure. Veámos lo siguiente:

Ejemplo:

 
Ahora el valor de i está correctamente almacenado:
 

Este es el código:

<div class="ejemplo-linea" id="ejlin2">
   <input type="button" value="Crear 3 div" onclick="construirHtml2()" />
   <div id="div-div2" style="border: gray solid 1px; padding: 0.2em">&nbsp;</div>
   Ahora el valor de <code>i</code> está correctamente almacenado:
   <pre id="mensaje-click2" style="border: gray solid 1px">&nbsp;</pre>
</div>
<script>
    function construirHtml2(){
        var divDiv = document.getElementById("div-div2");
        divDiv.innerHTML = "&nbsp;";
        document.getElementById("mensaje-click2").innerHTML = "&nbsp;";
        for (var i=0, elemento; i<3; i++){
            elemento = document.createElement("div");
            elemento.title = i;
            elemento.style.border = "blue solid 1px";
            elemento.style.display = "inline block";
            elemento.innerHTML = "Elemento " + i;
            elemento.onclick = function(n){
                return function(){
                    document.getElementById("mensaje-click2").innerHTML = 'ONCLICK elemento ' +
                    '<span class="resalte-amarillo">i = ' + n + '</span>' +
                    " (Title = " + this.title + ")";
                };
            }(i);
            divDiv.appendChild(elemento);
        }
    }
</script>

Ahora en cada iteración se preserva el valor de la variable i. La solución se basó en tratar la variable i para que no sea una variable libre al ejecutar cada iteración del bucle. ¿Y esto cómo se hace? Apliquemos esto a un pequeño ejemplo en JavaScript con una estructura similar al problema anterior:

Ejemplo:

devuelve function(){return x;}
devuelve 9999, la variable libre

El código es el siguiente que comentaremos luego:

<div class="ejemplo-linea">
    <input type="button" class="codigo" value="alert(y)" onclick="alert(y)" />
        devuelve <code>function(){return x;}</code><br />
    <input type="button" class="codigo" value="alert(y())" onclick="alert(y())" />
        devuelve <var>9999</var>, la <strong>variable libre</strong>
</div>
<script>
    function construir1(){
        var x = 1;
        //x es variable libre de temp
        var temp = function(){
            return x;
        };
        //Modificamos x
        x = 9999;
        //Devolvemos la función
        return temp;
    }
    //Construimos la función
    var y = construir1();
</script>

La función construir1() devuelve otra función creándose un closure con la variable libre x. Antes de devolver la función interior y después de construida modificamos la x con el valor 9999. El botón del ejemplo nos dará 9999 y no 1, pues la variable libre finalizó la ejecución de construir1() con 9999. Por lo tanto x es una variable libre para la función devuelta. Veámos como se resuelve:

Ejemplo:

devuelve function(){return n;}
devuelve 1, ahora NO es la variable libre, sino el valor asignado en el momento en que se construyó la función que se devuelve.

Código:

<div class="ejemplo-linea">
    <input type="button" class="codigo" value="alert(z)" onclick="alert(z)" />
        devuelve <code>function(){return n;}</code><br />
    <input type="button" class="codigo" value="alert(z())" onclick="alert(z())" />
        devuelve <var>1</var>, ahora NO es la variable libre, sino el
        <strong>valor</strong> asignado en el momento en que se construyó la función
        que se devuelve.
</div>
<script>
    function construir2(){
        var x = 1;
        var temp = function(n){
            //n NO es la variable libre x
            return function(){
                return n;
            };
        }(x);
        //Modificamos x
        x = 9999;
        //Devolvemos la función
        return temp;
    }
    var z = construir2();
</script>

Ahora componemos dos funciones anónimas. En la interior tenemos la que finalmente se devolverá, pero cambiando la x por otra variable, n. La exterior aplica el valor de x que en ese momento tenga para construir la función interior. Vea que la función exterior tiene la estructura function(n){...}(x). Los paréntesis al final suponen una llamada a esa función, asígnando el valor entre paréntesis al argumento n de la función. Realmente es un efecto closure ahora deseado, siendo ahora el argumento n, cuyo valor en ese momento es el de x, la variable libre para la función interior. De esta forma el argumento n=1 queda "congelado" cuando devolvemos la función interior a la variable temp.

Podemos también usar call() o apply(), métodos de Function.El ejemplo anterior con call() daría el mismo resultado aunque no supone ninguna ventaja aparente:

Ejemplo:

devuelve function(){return n;}
devuelve 1

function construir3(){
    var x = 1;
    var temp = (function(n){
        //n NO es la variable libre
        return function(){
            return n;
        };
    }).call(this, x);
    //Modificamos x
    x = 9999;
    //Devolvemos la función
    return temp;
}
var w = construir3();

Ahora call(this, x) aplica una lista de argumentos separados por comas a los argumentos de la función, en este caso sólo uno. Lo que estamos ejecutando es una llamada a la función cambiando el argumento n por x. La función interior queda construida con el valor de x en ese momento, blindándose por el efecto de este closure ahora deseado como en el ejemplo anterior.

El problema de las fugas de memoria (Memory Leak)

El problema de las fugas de memoria ocurre cuando una zona de memoria que se ha estado usando no es liberada tras finalizar la ejecución de una programa, no quedando ninguna referencia a esa memoria. El efecto closure puede convertirse en una fuga de memoria cuando la referencia externa es eliminada y aún permacen la variables en el closure. El problema puede aparecer por varias causas, siendo una cuando se crean referencias circulares no deseadas o accidentales. Por ejemplo, un objeto tiene una propiedad que referencia otro segundo objeto y este a su vez referencia al primero. Mientras se mantenga esta cadena de referencias no se puede eliminar ninguna de las zonas de memoria de uno u otro objeto. Con dos referencias la cantidad de memoria fugada puede no ser mucha, pero si intervienen un número grande puede incluso bloquear la ejecución de toda la página.

El problema es mucho más complejo que este breve resumen y merecería analizarlo aparte de estos temas. Especialmente porque no es fácil crear ejemplos de fugas de memoria y visualizar lo que está pasando.

Los métodos call y apply para funciones

En este apartado haré una introducción al método call ya que salió en un ejemplo más arriba y aunque no tenga relación con el tema de Closures. El método apply funciona igua que call pero los argumentos se pasan en un array en lugar de pasarlo como valores separados por comas. La idea general de estos métodos es aplicar una función en un contexto particularizado. Con función.call(objeto, argumentos) se llama a la función sobre un objeto con unos argumentos particulares, de tal forma que el valor this hará referencia a ese objeto y no al valor donde fue creada la función.

Ejemplo:

 
<div class="ejemplo-linea">
    <ol>
        <li><input type="button" class="codigo"
            value="fGlobal(123)"
            onclick="fGlobal(123)" />
        </li>
        <li><input type="button" class="codigo" title="Title del INPUT"
            value="fGlobal.call(this, 123)"
            onclick="fGlobal.call(this, 123)" />
        </li>
        <li><input type="button" class="codigo"
            value="fGlobal.call(fGlobal[this], 456)"
            onclick="fGlobal.call(fGlobal[this], 456)" />
        </li>
        <li><input type="button" class="codigo"
            value="fGlobal.call(objeto1, 123)"
            onclick="fGlobal.call(objeto1, 123)" />
        </li>
        <li><input type="button" class="codigo"
            value="fGlobal.call(objeto2, 123)"
            onclick="fGlobal.call(objeto2, 123)" />
        </li>
    </ol>
    <pre id="mens-call" style="border: gray solid 1px;">&nbsp;</pre>
</div>
<script>
    //Una variable global
    var title = "Variable Global";
    //Una función global, this.title será la variable global title
    function fGlobal(arg){
        document.getElementById("mens-call").innerHTML =
            'this = <span class="azul">' +this +
            '</span><br />this.title = <span class="azul">' + this.title +
            '</span><br />arg = <span class="azul">' + arg + '</span>';
    }
    //Un objeto literal, this.title = objeto1.title
    var objeto1 = {title: "Valor de title en un objeto literal"};
    //Una función como objeto, this.title = objeto2.title
    function fObj(){
        this.title = "Valor de title con el constructor new fObj()";
    }
    var objeto2 = new fObj();
</script>

Supongamos que tenemos una función fGlobal(arg) que queremos usar en diferentes contextos. Esta función nos devolverá la concatenación del string del objeto this, this.title y el argumento arg que se le pase. Los botones del ejemplo ejecutan una acción en su evento onclick. La primera llama a fGlobal(123). La primera respuesta nos dice que el objeto es Window y que this.title en este contexto se refier a la variable global title.

El segundo botón llama a fGlobal.call(this, 123). Como esto está dentro del evento onclick de un elemento <input type="button">, el this se refiere al propio elemento. El title extraido será ese atributo del elemento. Para volver a referirnos a Window en este contexto hemos de hacer fGlobal.call(fGlobal[this], 456), como se ve en el tercer botón. Esto es porque cuando se crea la función fGlobal se asigna automáticamente una propiedad this que apunta a Window.

El uso de call está más orientado a aplicar una misma función sobre diferentes objetos. En los botones cuarto y quinto se aplica sobre dos objetos, el primero un objeto literal y el segundo un objeto construido con el operador new.