Closures en JavaScript

JavaScript functions
A veces llamado Lexical scoping o Static scoping, un closure es un tipo especial de objeto que combina dos cosas: una función, y el entorno en que se creó esa función. Es decir que una función definida dentro del closure “recuerda” el entorno en el que se ha creado y tiene acceso a las variables de ese entorno (scope).MDN

Éstas son algunas características del closure:

  • El closure permite encapsular el código.
  • El contexto de una función anidada incluye el scope de la función externa.
  • El entorno está formado por las variables locales dentro del ámbito (scope) cuando se creó el closure (variables libres).
  • Una función anidada sigue teniendo acceso al contexto de la función externa, incluso después de que ésta haya retornado.

Antes de continuar, vamos a repasar algunos conceptos.

Funciones: objetos de primera clase

En JavaScript las funciones son objetos de primer nivel (higher-order-functions) o ciudadanos de primera clase (first-class-citizen), ¿pero que significa esto? Vamos a revisar algunas de sus características.

  • pueden ser almacenados en variables y estructuras de datos
  • pueden ser pasados como parámetros de una función
  • pueden ser retornados como el resultado de una función
  • pueden ser construidos en tiempo de ejecución
  • poseen transparencia referencial
  • pueden ser anónimos

Así que en JavaScript, las funciones tienen un trato especial y éstas características son indispensables en el paradigma de programación funcional. Una función es un objeto cuyo constructor es Function

Scope: alcance o ámbito

El alcance de una variable en JavaScript se extiende en el contexto de la función en la que fue declarada (function-level scope), es decir, si declaro un variable dentro de una función solo será visible dentro de la misma, incluyendo todos los bloques que estén dentro de ella, a diferencia de otros lenguajes de programación (familia C), en donde el ámbito de las variables tiene alcance hasta el bloque en el cual fueron definidas (block-level scope), por ejemplo, dentro de un bloque de instrucciones iffor, while; Pueden ver algunos ejemplos en el artículo ¿Qué es hoisting?

Entrando en materia

Hasta el momento solo hemos visto la definición de closure y para explicarlo, se ha empleado bastante el término scope / scoping; si tiene claro el concepto, puede continuar la lectura, de lo contrario, recomiendo despejar dudas leyendo el artículo JavaScript Scoping and Hoisting por Benn Cherry.

Veamos el siguiente ejemplo. Tenemos una función que recibido un número (1-12), retorna un texto con el mes correspondiente:

var months = [
        "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
        "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];

function getMonth (n) {
    //evita bug al acceder elementos fuera del rango
    if (n < 1 || n > 12)
        throw new RangeError("Mes incorrecto");
    return months[n-1];
}

alert( getMonth(3) );

Desafortunadamente aquí, la variable months está en el global scope, y el problema es que al ser global, cualquier otra función puede acceder a ella y modificarla, además que depender de variables globales va en contra del principio de bajo acoplamiento.

Ahora vamos a reescribir el código anterior estableciendo la variable months como un objeto privado dentro de la función, de manera que no sea accesible desde afuera.

function getMonth (n) {
    var months = [
        "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
        "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];

    if (n < 1 || n > 12)
        throw new RangeError("Mes incorrecto");
    return months[n-1];
}

alert( getMonth(12) );

Desafortunadamente aquí, cada vez que llamamos a la función, la variable months es creada en memoria, lo cual es una mala práctica porque representa un desperdicio en el manejo de recursos — Memory Management.

Recordemos que un closure está compuesto por funciones que retienen la referencia de las variables libres (las variables locales dentro del ámbito / scope del closure)

Ahora vamos a crear la función getMonth como un closure IIFE que mantenga privado el array months pero sin que éste sea creado en cada llamado.

var getMonth = (function() {
    var months = [
        "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
        "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];

    return function(n) {
        if (n < 1 || n > 12)
            throw new RangeError("Mes incorrecto");
        return months[n-1];
    };

}()); //end IIFE

alert( getMonth(12) );

En el código anterior tenemos una función IIFE contenida entre paréntesis que es ejecutada inmediatamente y retorna otra función, por lo tanto el valor retornado es asignado a la variable getMonth

El truco para que la función externa sea inmediatamente ejecutada y el valor retornado sea asignado, es encerrar la definición de la función entre paréntesis, de este modo indicamos que se va a ejecutar una function expression ya que los paréntesis no pueden contener declaraciones (ej. function declaration). Recomiendo leer el artículo Immediately-Invoked Function Expression (IIFE) de Ben Alman.

A tener en cuenta

Quiero hacer un llamado a que sigamos el principio de desarrollo KISS y no agreguemos complejidad cuando no es necesaria; los closures sin duda son una de las características más poderosas de JavaScript y dominarlos requiere que aprendamos más del ecosistema del lenguaje y no solo hablo de código, sino de performance, memory management y del garbage collector, ya que un uso indebido puede propiciar fugas en memoria (memory leak).

“With great power comes great responsibility”

Recuerden que un closure mantiene en memoria los objetos (variables libres) creados al interior del closure siempre y cuando sean accedidos por la función interna, esto significa que mientras mantengamos una referencia a la función interna, las variables libres no podrán ser liberadas por el garbage collector, lo cual es una ventaja si se sabe utilizar, o puede ser una amenaza, si se usa con descuido. Ver notas sobre Garbage Collection y Memory leaks.

Si no va a utilizar más un objeto definido dentro del closure, es una buena práctica hacer un dispose de ese objeto para permitir que el garbage collector recupere la memoria asignada, a no ser que hablemos de una aplicación real-time, en cuyo caso debemos minimizar la sobrecarga del garbage collector (ver nota How to write low garbage real-time Javascript).

Tipos de closure

Un Closure también nombrado como Lexical Scoping o Static Scoping, define cómo los nombres de variables se resuelven en funciones anidadas. Esto significa que el alcance (scope) de una función interna contiene el ámbito de la función padre, incluso si ésta ya ha retornado.

Ejemplo #1

Necesitamos guardar el momento en que una función fue llamada por primera vez:

//inicio closure
function init (name) { //función padre

    //esta variable solo se incializa
    //la primera vez que se invoca init()
    var _time = +new Date();

    //lazy function definition
    init = function(name) { //función anidada
        console.log("User: " + name + " >");
        console.log("Init time: " + _time);
    }

    init(name);
}
//fin closure

init("David");
setTimeout(function() {
    init("Jherax");
}, 500);

En este ejemplo vemos que la función padre no retorna ningún valor, y ha sido refedinida en su interior para mantener el acceso al scope (Lazy Function Definition Pattern). El closure será creado sólo la primera vez que se invoque la función init


Ejemplo #2

Vamos a crear una función que permita rellenar con caracteres el texto proporcionado. El valor agregado es que podemos configurar los caracteres de relleno y la cantidad total de caracteres que debe retornar la función interna.

//inicio closure
function paddingLeft (quantity, fillchar) { //función externa
    quantity = quantity || 2;
    fillchar = fillchar || "0";

    return function (text) { //función interna
        var filled = new Array(quantity).join(fillchar) + text;
        return filled.slice(-quantity);
    };
}
//fin closure

var zeroPad = paddingLeft();
console.log( zeroPad(9) );

var dotPad = paddingLeft(20, ".");
console.log( dotPad("pg 13") );
console.log( dotPad("pg 17") );

En este ejemplo, la función externa sí retorna un valor, y es la función interna, por lo tanto cada vez que invoquemos la función paddingLeft, ésta creará un nuevo closure que retorna una función, la cual mantiene el estado interno de las variables quantity, fillchar recibidas por la función padre.

Code: closure
Image: closure

En la gráfica tenemos una versión más clara del scoping de la aplicación.


Ejemplo #3

Crearemos un constructor que permita notificar errores personalizados, el cual reciba múltiples argumentos para construir un mensaje de error. Recomiendo que revisen la versión mejorada en el siguiente stackoverflow.

//inicio closure IIFE
var CustomError = (function() { //función externa
    'use strict';

    function CustomError(message) { //función interna retornada
        var i, error, len;

        //construye el mensaje con múltiples argumentos
        message = message || "Ha ocurrido un error";
        for (i = 1, len = arguments.length; i < len; i += 1)
            message = message.replace(
                new RegExp("\\{" + (i - 1) + "}"), arguments[i]);

        //crea el stack del error
        error = new Error(message);

        //accede a CustomError.prototype.name
        error.name = this.name;
        this.stack = error.stack;
        this.message = message;
    }

    //previene una referencia directa a Error.prototype
    CustomError.prototype = Object.create(Error.prototype);
    Object.defineProperties(CustomError.prototype, {
        //corrige el constructor
        "constructor": {
            enumerable: false,
            value: CustomError
        },
        "name": {
            enumerable: false,
            value: "Custom Error"
        }
    });

    //retorna el constructor
    return CustomError;
}());
//fin IIFE

//lanzamos una excepción personalizada
var a = "sultán", b = "escupitajo";
throw new CustomError("El {0} del {1}", a, b);

En este caso la función externa es un IIFE que es ejecutado inmediatamente y retorna la función interna CustomError. Aquí el closure es creado sólo una vez, en el momento en que asignamos el valor retornado por el IIFE, esto significa que aunque creemos varias instancias del constructor CustomError, no se volverá a crear el closure. Éste también es un ejemplo básico del patrón Constructor.


Poniendo en práctica

Vamos a trabajar el siguiente caso. Supongamos que tenemos una variable con un documento HTML como string y queremos que una función nos devuelva ciertos elementos DOM. Para ello desarrollaremos un closure:

  • una función externa que reciba un argumento con el documento HTML-string.
  • una función interna que reciba un argumento con el selector de los elementos DOM que necesitamos obtener.

Para este ejemplo vamos a utilizar la librería jQuery.

//inicio CLOSURE
//creamos la función que reciba
//el documento HTML como un string
function getDocument (htmlString) {
    
    //creamos el objeto jQuery con los
    //elementos DOM representados en htmlString 
    //y creamos un wrapper para asegurarnos que 
    //exista un root en el documento
    var htmlDoc = 
            htmlString ?
            jQuery("<div>" + htmlString + "</div>") :
            jQuery();

    console.log("htmlDoc:\n", htmlDoc);

    //creamos la función que retorna los elementos DOM
    //que coincidan con el selector provisto
    return function (selector) {
        var nodes = htmlDoc.find(selector);
        return nodes;
    };
}
//fin CLOSURE

//supongamos que leímos el archivo usando jQuery.ajax()
//y retornó el siguiente documento como string:
var html = [
    '<section id="main">',
        '<article id="first">',
            '<h2>Resumen artículo 1</h2>',
            '<div class="abstract">',
                '<p>Lorem ipsum dolor sit amet</p>',
            '</div>',
        '</article>',
        '<article id="second">',
            '<h2>Resumen artículo 2</h2>',
            '<div class="abstract">',
                '<p>blah blah blah</p>',
            '</div>',
        '</article>',
    '</section>'
].join("");

//invocamos la función closure
var ley100 = getDocument(html);

//utilizamos la función retornada
var art1 = ley100("#first").get(),
    art2 =ley100("#second").get(),
    titulos = ley100("h2").get(),
    resumen = ley100(".abstract").get();

console.log("\nObjetos encontrados");
console.log([art1, art2, titulos, resumen]);

Ahora, si queremos obtener los objetos jQuery en vez de los objetos DOM, solo debemos remover el método .get() de nuestras variables.

Conclusión

En este artículo aprendimos a utilizar uno de los pilares de la programación avanzada en JavaScript. Los closures son una técnica de encapsulamiento que permiten crear privacidad y optimizar los procesos de asignación de memoria y el garbage collector, ya que podemos mantener el estado interno de los objetos en chaché dentro del closure. Es esencial e importante comprenderlos para mejorar nuestro nivel de JavaScript, y en la medida que mejoramos, los utilizaremos de forma natural.

Encontré unos artículos muy buenos, y recomiendo que saquen un tiempo para leerlos: Closures en JavaScript: entiéndelos de una vez por Óscar Sotorrío, Contextos y encapsulamiento en JavaScript de Pablo Molina y Closures de MDN.

Happy coding!

13 thoughts on “Closures en JavaScript

    • 😉 gracias bro, ando fundido en trabajo y no he podido volver a escribir, sin embargo tienes a disposición un par de interesantes artículos que he escrito… y como dice Terminator: “I’ll be back!”

  1. Ricky says:

    Hola Jherax, muy bueno tu tutorial, de lo mejor que he encontrado respecto a javascript, gracias por eso. Tengo una pregunta: En el 2do ejemplo que planteas utilizas el método slice y no llego a entender cuál es su utilidad, ya que utilizando solo “join” ya tenemos una cadena con los caracteres de relleno y el texto proporcionado desde fuera del closure ¿para qué usar slice?

    saludos,

    Ricky

    • Hola Ricky, el método slice se utiliza con el fin de extraer una determinada cantidad de caracteres, en el caso que tu mencionas, al hacer un padding-left significa que vamos a rellenar hacia la izquierda una determinada cantidad de caracteres, por eso pasamos un valor negativo al método slice, para que tome los caracteres desde el final de la cadena (desde la derecha) por ejemplo:

      var filled = "............pagina 1",
          quantity = 12;
      console.log( filled.slice(-quantity) );
      
  2. Josue says:

    Gracias por la información, me ha sido muy útil para dar el paso a la programación avanzada con javascript.

Comentarios

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s