Cómo funciona un closure de JavaScript

A veces la mejor forma de entender un concepto es hacer un ejemplo. Y creo que se esto es cierto para los Closures. El siguiente ejemplo pone en ejecución un closure. A continuación hay algunos botones que devuelven un resultado explicándose en el texto su significado.

Ejemplo:

Ver typeof(temporalClosure)          (respuesta: undefined)
Ver typeof de closure1 y closure2          (respuesta: function, function)
¿Es closure1 === closure2?          (respuesta: false)
Ejecutar closure1("jklmnopq")         (respuesta: abcDEFjklmnopq)
Ejecutar closure2("4567890")         (respuesta: abc1234567890)

El código HTML y JS de lo anterior es el siguiente. Observe especialmente el código del script que comentaré a continuación.

<div class="ejemplo-linea">
    <div>Ver <code>typeof(temporalClosure)</code>
        <input type="button" value="="
        onclick="this.nextSibling.innerHTML=typeof(temporalClosure)"
        /><span class="azul"
        style="border-bottom: dotted 1px">&nbsp;</span>
        (respuesta: <b>undefined</b>)
    </div>
    <div>Ver <code>typeof</code> de <code>closure1</code> y <code>closure2</code>
        <input type="button" value="="
        onclick="this.nextSibling.innerHTML=typeof(closure1) + ', ' + typeof(closure2)"
        /><span class="azul"
        style="border-bottom: dotted 1px">&nbsp;&nbsp;&nbsp;</span>
        (respuesta: <b>function, function</b>)
    </div>
    <div>¿Es <code>closure1 === closure2</code>?
        <input type="button" value="="
        onclick="this.nextSibling.innerHTML=(closure1===closure2)"
        /><span class="azul"
        style="border-bottom: dotted 1px">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>
        (respuesta: <b>false</b>)
    </div>
    <div>Ejecutar <code>closure1("jklmnopq")</code><input
        type="button" value="="
        onclick="this.nextSibling.innerHTML=closure1('jklmnopq')"
        /><span class="azul"
        style="border-bottom: dotted 1px">&nbsp;</span>
        (respuesta: <b>abcDEFjklmnopq</b>)
    </div>
    <div>Ejecutar <code>closure2("4567890")</code><input
        type="button" value="="
        onclick="this.nextSibling.innerHTML=closure2('4567890')"
        /><span class="azul"
        style="border-bottom: dotted 1px">&nbsp;</span>
        (respuesta: <b>abc1234567890</b>)
    </div>
</div>
<script>
    function crearClosure(argumento){
        var local = "abc";
        function temporalClosure(argumentoClosure){
            return (local + argumento + argumentoClosure);
        }
        return temporalClosure;
    }
    var closure1 = crearClosure("DEF");
    var closure2 = crearClosure("123");
</script>

En primer lugar tenemos una función exterior llamada crearClosure(argumento). Dispone de una variable local y una función interior con nombre temporalClosure que es devuelta con return temporalClosure. La función interior concatena los valores de la variable local, el argumento de la función exterior y el argumento de esa función interior. La función exterior es llamada y su resultado es asignado a las variables closure1 y closure2 cuando se carga la página. Estas variables contienen ahora referencias a la función interior temporalClosure(argumentoClosure). Con el primer botón del ejemplo comprobaremos que temporalClosure no es accesible pues nos da undefined, pero aún así tendremos acceso a esa función.

Los tipos de closure1 y closure2 son function, es decir, son funciones. Ambos son diferentes como vemos con el botón que pregunta si son la misma cosa. Al ejecutar la llamada a una función closure1("jklmnopq") obtenemos el String "abcDEFjklmnopq". Esto es la concatenación del valor "abc" de la variable local de la función externa, más el valor "DEF" del argumento de la función externa que le pasamos cuando construimos var closure1 = crearClosure("DEF") y por último el valor "jklmnopq" que pasamos cuando llamamos a closure1("jklmnopq") con el botón.

Por lo tanto de lo que se retorna local + argumento + argumentoClosure permanecerá constante local y argumento tras haber construido el closure con var closure1 = crearClosure("DEF"). Es un efecto inesperado que podamos acceder a variables locales y sus argumentos de una función ya ejecutada. Esto sucede únicamente porque estamos devolviendo la referencia a la función interior. Y esta a su vez tiene referenciadas la variable local y el argumento de la función exterior. Cuando JavaScript encuentra una referencia a una función mantiene los valores asignados en otros alcances, en este caso en el alcance de la función exterior, incluso después de que esa función exterior haya finalizado.

Podemos usar las herramientas de desarrollo (developer tools) del navegador Chrome para inspeccionar el closure. En este caso ponemos un punto de interrupción en la ejecución de la función tras pulsar el botón con la la llamada closure1("jklmnopq"). El Call Stack nos indica la pila de llamadas donde se observa que proviene del evento onclick del botón. El Scope Variables es el alcance de la función interior temporalClosure.

Cómo funciona un closure

Se observan en el alcance los siguientes grupos de variables:

  • Local: Son las variables locales de la función interior temporalClosure. En el código no pusimos ninguna, pues lo único que hay es el return (local + ...);. Pero existe el argumentoClosure, pues los argumentos de una función son también variables locales para esa función.
  • this: Referencia al objeto Window, el espacio Global de las variables globales.
  • Closure: Aquí se encuentran todas las variables libres del closure. Está el argumento que pasamos en la función constructora crearClosure así como la variable llamada local (con minúscula, nada tiene que ver con el grupo "Local") que también declaramos en la función constructora con var local = "abc".
  • Global: El espacio de variables globales que viene ser lo mismo a lo que apunta this: el objeto Window.

Las variables del closure permacen blindadas en ese espacio, de tal forma que no serán accesibles desde el exterior. Sólo podemos acceder a ellas para verlas o modificarlas con funciones del propio closure. Es fácil ver el potencial de un closure:

  • Los valores de la variable local y del argumento de la función exterior quedan definidos cuando se construye el closure. Son las variables libres del closure. Podemos modificar dinámicamente esa estructura asignando otros argumentos a la función exterior y obteniendo una nueva función interior.
  • Las variables locales de la función exterior no podrán ser accedidas sino a través del closure. Esto es útil para encapsular datos de tal forma que sólo puedan ser leídos o modificados usando el closure.

Closures y funciones anónimas

Una ligera modificación del ejemplo anterior en la devolución de la función interior nos ahorra algo de código, pues al fin y al cabo la referencia temporalClosure ya no es accesible en el exterior, como demuestra la acción del primer botón que resultó undefined. Volvemos a repetir el ejemplo para crearClosure("123") que asignaremos a una nueva variable closure3:

Ejemplo:

Ejecutar closure3("4567890")         (respuesta: abc1234567890)

Obtenemos el mismo resultado con este código donde eliminamos temporalClosure devolviendo directamente una función anónima:

<div class="ejemplo-linea">
    <div>Ejecutar <code>closure3("4567890")</code><input
        type="button" value="="
        onclick="this.nextSibling.innerHTML=closure3('4567890')"
        /><span class="azul"
        style="border-bottom: dotted 1px">&nbsp;</span>
        (respuesta: <b>abc1234567890</b>)
    </div>
</div>
<script>
    function crearClosure2(argumento){
        var local = "abc";
        return function(argumentoClosure){
            return (local + argumento + argumentoClosure);
        };
    }
    var closure3 = crearClosure2("123");
</script>

Este es el motivo por lo que se usan funciones anónimas para estructurar el closure, pues la función interior se devolverá como una referencia y no vamos a necesitar el nombre de la misma. Es más, ni siquiera será accesible. Por eso indiqué en el primer apartado que no hay que confundir los closures con las funciones anónimas, aunque son dos conceptos que pueden trabajar juntos.

En algún caso podemos ahorrarnos todavía un paso en la creación del closure asignado directamente a la variable. Volviendo a repetir el ejemplo anterior podemos pasar la última sentencia que declara crearClosure("123") e incluirla con la declaración de la función.

Ejemplo:

Ejecutar closure4("4567890")         (respuesta: abc1234567890)

Ahora con var closure4 asignamos directamente la función anónima y luego la auto-ejecutamos para el argumento ("123").

<div class="ejemplo-linea">
    <div>Ejecutar <code>closure4("4567890")</code><input
        type="button" value="="
        onclick="this.nextSibling.innerHTML=closure4('4567890')"
        /><span class="azul"
        style="border-bottom: dotted 1px">&nbsp;</span>
        (respuesta: <b>abc1234567890</b>)
    </div>
</div>
<script>
    var closure4 = function(argumento){
        var local = "abc";
        return function(argumentoClosure){
            return (local + argumento + argumentoClosure);
        };
    }("123");
</script>

Esta última forma es útil cuando el closure no cambia dinámicamente, pues se construye en el momento en que se ejecuta ese código y no como en los ejemplos anteriores que creábamos el closure con un función auxiliar constructora como crearClosure(). En ese caso podíamos volver a crear otra vez el closure con otro argumento, lo que puede ser interesante si tenemos que modificar el closure en otro punto del código.

Pero, ¿Qué es un closure?

Este tema de los closures es bastante complejo cuando uno intenta verlo como una característica estructural más que de funcionamiento del lenguaje. Esta cita de doble definición de closure de Dimitry Shoshnikov y que traduzco a continuación nos puede aclarar algo:

A menudo se simplifica los closures sólo a funciones internas devueltas desde una función externa donde se preserva ese contexto. Una simplificación aún más incompleta es que los closures pueden ser sólo funciones anónimas. Todas las funciones son closures debido al mecanismo de la cadena de alcance, a excepción de las que se crean con el constructor new Function. Dos definiciones correctas de closure en relación con ECMAScript son:

  • Desde un punto de vista teórico: Cualquier función es un closure si en el momento de su creación guarda variables de un contexto padre de esa función. Incluso una simple función global que referencie una variable global supone que esta es una variable libre y por tanto se usa el mecanismo de la cadena de alcance (siendo por tanto un closure).
  • Desde un punto de vista práctico: Nos interesan las funciones que
    1. Siguen existiendo después de que su contexto padre ha sido ejecutado, como son las funciones internas devueltas desde una función externa.
    2. Y usan variables libres del contexto de la función externa.

No se puede perder de vista como funciona el alcance de las variables, que nos ayudará a resolver un problema sin ni siquiera tener que mencionar el concepto del closure. De hecho podríamos incluso olvidarnos de este término y pensar en como JavaScript resuelve los problemas del alcance de las variables y del Funarg expuestos en el primer tema. Pero como estos temas van de closures, cuando me refiera a ellos será desde el punto de vista práctico señalado en esa definición de Dimitry Shoshnikov.


Los siguientes apartados sobre funciones auto-ejecutables y el operador coma de JavaScript no tienen relación directa con los closures, pero como han surgido en los ejemplos anteriores creo que merece la pena exponerlos. Si ya los conoce puede ir al siguiente tema.

Funciones auto-ejecutables

En JavaScript podemos declarar una función con function f(args){...}. Pero también podemos asignar una función anónima a una variable con var f = function(args){...};. Se observa que la primera es una declaración y esta segunda es una expresión de asignación, es decir, asigna el objeto Function a la variable. Recuerda que Function es también un Object y puede ser asignado. Pero en los dos casos la función queda referenciada con el identificador f. En el caso de una función anónima podemos llamar a la función asignando valores a los argumentos en tiempo de ejecución con var f = function(args){...}(values);, siendo values una lista de valores que se adjudican a la lista de argumentos. Esto es un función auto-ejecutable, pues se ejecuta inmediatamente después de ser construida. En inglés podemos encontrar el término self-executing. También como self-invoked (auto-invocadas o auto-llamadas), o incluso abreviadamente como IIFE (immediately-invoked function expression).

En el ejemplo var f = function(args){...}(values); la devolución f no es un tipo function como es que el devuelve var f = function(args){...};. El tipo de dato ahora será el que se retorne de la llamada a la función. Podría ser un tipo string, number, etc. o function si devuelve a su vez una función, incluso undefined si no devuelve nada. Veámos este ejemplo:

Ejemplo:

Este es el código:

<div class="ejemplo-linea">
    <input type="button" value="ver f1('b')" class="codigo"
    onclick="alert(f1('b') + ', Tipo f1 es ' + typeof(f1))" />
    <input type="button" value="ver f2" class="codigo"
    onclick="alert(f2 + ', Tipo f2 es ' + typeof(f2))" />
</div>
<script>
    var f1 = function(z){
        return "a"+z;
    };
    var f2 = function(z){
        return "a"+z;
    }("b");
</script>

Ambos ejemplos usan funciones anónimas asignadas a una variable. El primer botón nos devolverá a la llamada f1("b") un tipo String con el valor ab, pues concatena y devuelve return "a"+z. Pero f1 es un tipo Function, una función. El segundo botón nos devolverá el resultado de f2, que será el mismo String ab, pero f2 es un String pues hemos asignado el return "a"+z a f2. Esto es así porque dotamos de valor al argumento z cuando la función es auto-ejecutada acompañando ("b") al final de la misma. A partir de aquí tendremos en f2 el tipo de dato devuelto, en este caso un String.

En la ejecución de la función auto-ejecutable no rodeamos la función con paréntesis aunque no está de mal hacerlo para, digamos, ver de entrada que es una función auto-ejecutable. Quedaría como var f = (function(args){...})(values);. Los paréntesis no afectan a la ejecución y es una ayuda para leer el código, pues si una función tiene muchas líneas al principio estará var f = function(args){ y no sabremos si es o no auto-ejecutable hasta llegar al cierre }(values);. De esta forma se asemejará al caso cuando tengamos una función anónima sin devolución y auto-ejecutable, donde será obligatorio poner los paréntesis: (function(args){...})(values).

A continuación hay algunos ejemplos de funciones auto-ejecutables, acompañando algunos comentarios sobre cada caso.

Ejemplo:

Ejemplos de auto-ejecutables:
  1. No anónima y sin devolución:
  2. Anónima y sin devolución:
  3. Anónima y Con devolución:
  4. Anónima y Con devolución en variable global:
  5. Anónima y Sin devolución y prefijada:
  6. Anónima y Sin devolución y con void:
  7. No anónima con auto-devolución: .
    Se devuelve a sí misma asignándola a la variable estaFunción con el tipo .
    Con este podemos ejecutar otra vez la función devuelta haciendo estaFuncion(Date()) que pondrá la fecha-hora actual en el elemento.
// EJEMPLOS DE FUNCIONES AUTO-EJECUTABLES

// 1)La función no es anónima, se ejecuta y no devuelve nada
// Los paréntesis son obligatorios.
(function nominada(arg){
    document.getElementById("iife-1").innerHTML = arg;
})("1-ABC");

// 2) La función anónima se ejecuta y no devuelve nada.
// Los paréntesis son obligatorios.
(function(arg){
    document.getElementById("iife-2").innerHTML = arg;
})("2-DEF");

// 3) La función es anónima, ejecuta algo y devuelve un resultado. No son
// necesarios los paréntesis de la función pero es aconsejable.
var resultado = (function(arg){
    document.getElementById("iife-3").innerHTML = arg;
    return "Resultado: " + arg;
})("3-GHI");

// 4) No se devuelve nada, pero el resultado se puede asignar
// a una variable global. Paréntesis obligatorios.
var resultado = "";
(function(arg){
    document.getElementById("iife-4").innerHTML = arg;
    resultado = "Resultado: " + arg;
})("4-JKL");

// 5) Sin devolución y sin paréntesis prefijando la función con un operador unario.
// (ver http://www.ecma-international.org/ecma-262/5.1/#sec-11.4)
// Aplicar el operador hace que se resuelva la expresión como
// si la rodearamos con paréntesis. Creo que entorpece la lectura
// del código.
+function(arg){
    document.getElementById("iife-5").innerHTML = arg;
}("5-MNO");

// 6) Si no se quier usar paréntesis, mejor con el operador unario void
void function(arg){
    document.getElementById("iife-6").innerHTML = arg;
}("6-PQR");

// 7) Autoejecución y autodevolución: La función no es anónima, ejecuta algo
// y se devuelve a sí misma.
var estaFuncion = (function temp(arg){
    document.getElementById("iife-7").innerHTML = arg;
    return temp;
})("7-STU");
// Con esto vemos el tipo devuelto que es Function
document.getElementById("tipo-funcion").innerHTML = typeof estaFuncion;    

Todo esto entra en la materia del tratamiento de los paréntesis en JavaScript. La ejecución var f = function(args){...}(x); viene a ser como var f = algo(x). O bien (function(args){...})(x) viene a ser como algo(args)(values), donde ahora algo(args) denota la implícita existencia de una función temporal que se construye y se ejecuta en una misma sentencia. Cuando JavaScript encuentra que algo es una función entonces (x) será su argumento. Podríamos "complicarnos más la vida" usando expresiones con el operador coma. Con var f = (exp1, exp2, ..., expn)(x) tenemos que los expi son expresiones que se evalúan y luego se devuelve el valor evaluado por la última expresión expn. Si ésta última es una función (un tipo Function) entonces JavaScript entiende que (x) será su argumento de llamada. Vea este ejemplo:

var xx, yy;
var f3 = (xx = "b", yy = "c", function(z){return "a"+z;})(xx+yy);
alert(f3); //Muestra "abc"

Los primeros paréntesis contienen tres expresiones separadas por comas: dos asignaciones y una declaración de función anónima. Estos paréntesis devuelven la última expresión, es decir, una función. Los siguientes paréntesis contienen entonces los argumentos de la llamada a esta función, que tras ejecutarse devolverá el resultado final "abc".

El operador coma de JavaScript

La coma en JavaScript se usa para declarar variables con un sólo var, como var a=1, b=2, c=3;. También se usa para separar argumentos de funciones, elementos de un array literal con corchetes [ ] o propiedades de un objeto literal dentro de llaves { }. Pero el operador coma tiene otro significado cuando se encuentra en la parte derecha de una asignación y separando expresiones entre paréntesis. Esto se expone en la especificación ECMA-262-5.1 , en estos apartados:
  • 11.1 Primary Expressions, donde incluye (Expression) como una de las expresiones primarias. Estos paréntesis encerrando una expresión son denominados Grouping Operator que se expone en el apartado 11.1.6.
  • En 11.13 Assignment Operators se exponen los operadores de asignación, siendo algo como LeftHandSideExpression = AssignmentExpression. No es otra cosa que algo como var a = "123";. La parte derecha de la asignación es el llamado AssignmentExpression, en definitiva el valor que se asigna, que puede ser un literal de String como este ejemplo, una referencia o identificador o cualquier tipo de datos. Incluso cualquier objeto como un tipo Array o un tipo Function o cualquier operación o expresión que devuelva algo que pueda ser asignado a la parte izquierda.
  • Por otro lado el apartado 11.14 Comma Operator, el operador coma puede ser usado una o más veces tal como aparece definido de forma recursiva:
    Expression :
        AssignmentExpression
        Expression , AssignmentExpression 
    Así algo como una lista de expresiones var x = (exp1, exp2, exp3); se evalúa en parejas de dos, es decir, como var x = (exp1, (exp2, exp3));. Evaluaría exp1 y luego pasaría a evaluar (exp2, exp3), que siendo a su vez otra pareja de operadores evaluaría exp2 y exp3 devolviendo finalmente este último resultado.

En las expresiones entre paréntesis podemos poner cualquier cosa que pueda ser evaluada. Es decir, un AssignmentExpression que no es otra cosa que algo que devuelva un valor que pueda ser asignado. Por ejemplo (1<2, alert(1)) evalúa la primera expresión, saca un mensaje de alerta y devuelve undefined pues alert() no devuelve nada. Vea que el primer término 1<2 es evaluado pero no se hace nada con su resultado true. Si la evaluación implica la llamada a una función, ésta será ejecutada como es el caso de alert(). Pero (1<2, prompt("mensaje")) sacará el cuadro de diálogo para introducir un texto y éste será devuelto pues la función window.prompt() devuelve lo que el usuario introduce en la caja de texto.

Sin embargo algunos desaconsejan el uso del operador coma pues desestructura el código haciéndolo díficil de mantener. En algún caso es útil como cuando se usa dentro de un for. Por ejemplo for (var i=0, j=0; i<max; i++, j++), siendo la última coma este operador que evalúa y por tanto incrementa ambos índices para iterar por el bucle.