Closures en JavaScript

Closures
“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. El entorno está compuesto por las variables locales que están dentro del scope en el momento en que el closure fue creado” — MDN. Es decir que una función definida dentro del closure recuerda el entorno donde se creó y tiene acceso a las variables de ese entorno (variables libres).

Éstas características definen un closure, también llamado lexical scope o static scope:

  • El closure permite encapsular el código.
  • El entorno de una función anidada incluye el scope de la función externa.
  • El entorno está formado por las variables locales dentro de la función externa (variables libres) cuando se creó el closure.
  • Una función anidada sigue teniendo acceso al scope 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), esto significa que:

  • pueden ser almacenados en variables y estructuras de datos
  • pueden ser enviados como argumentos a una función
  • pueden ser retornados como el valor de una función
  • pueden ser construidos en tiempo de ejecución
  • poseen transparencia referencial (no cambian)
  • pueden ser anónimos (no tienen nombre)

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 tiene su prototipo en el constructor Function.

Scope: alcance o ámbito

En JavaScript (ES5-), el alcance de una variable está en la función donde fue declarada (function-level scope), es decir que si declaro una variable en cualquier parte dentro de una función, será visible en toda la función, esto incluye crear una variable dentro de cualquier bloque de instrucciones. A diferencia de otros lenguajes de programación, en donde el alcance/scope de las variables está limitado al bloque en el cual fueron definidas (block-level scope). Ver más en ¿Qué es hoisting?

Lexical Scope

Lexical-scope o static-scope tiene que ver con el lugar en donde el programador declara las variables en el código fuente, esto es function-level scope en JavaScript (ES5). Las funciones anidadas tienen acceso a las variables definidas en la función externa.

“Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope”

function scope of outer function === lexical scope of inner function

Entrando en materia

Hasta el momento solo hemos visto la teoría; si aún no tiene claro algunos conceptos o quiere profundizar más, recomiendo que lea el artículo Javascript’s lexical scope, hoisting and closures without mystery.

😛 De la teoría a la práctica. 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 menores a 0
  if (n < 1) throw new RangeError("Rango incorrecto");
  return months[n - 1];
}

console.log(getMonth(3)); // Marzo

Funciona, pero podemos encapsular el código para evitar contaminar el global scope. 😎 Como buenos programadores sigamos algunos principios GRASP como mantener alta cohesión y bajo acoplamiento, lo que significa encapsular el código relevante a una tarea, y disminuir la dependencia con el resto del entorno.

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

  if (n < 1) throw new RangeError("Rango incorrecto");
  return months[n - 1];
}

console.log(getMonth(9)); // Septiembre

Funciona!! Of course! La variable months ya no contamina el global scope y está contenida dentro de la función responsable. :/ Pero ahora tenemos un problema de performance, y es que cada vez que llamamos a la función getMonth, se crea y se destruye el array months, lo cual nos va a impactar por el proceso en que incurre la asignación de memoria de un nuevo objeto, y la marcación como “disposable” para el Garbage Collector (vea Memory Management). ⭐ Pensemos en grandes aplicaciones, o en enormes cantidades de elementos en un Array, en donde una función puede ser invocada cientos o miles de veces; por ejemplo, una función que esté pintando un frame y lo esté moviendo por la pantalla al seguir el puntero del mouse,… allí si importa el performance. 😳 Por lo tanto hagamos bien las cosas desde un principio y saquemos provecho de las características que cada lenguaje de programación nos ofrece.

😎 And here comes the closure bitches. Recordemos que en un closure, la función interna recuerda el entorno de la función externa, aún después de que ésta haya finalizado su ejecución. Por lo tanto vamos a crear un closure que mantenga vivo el array months, de modo que éste sólo se cree una vez y la función interna tenga acceso a él cada vez que es invocada.

var getMonth = (function iife() {
  // scope de iife === lexical scope de inner
  var MONTHS = [
    "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
    "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];

  return function inner(n) {
    if (n < 1) throw new RangeError("Rango incorrecto");
    return MONTHS[n - 1];
  };

}());

console.log(getMonth(12)); // Diciembre

💡 En el código anterior utilizamos un IIFE para crear el closure. Ésta función es creada y ejecutada inmediatamente para crear las variables libres dentro del closure, esto es, el array MONTHS. La función interna inner que retorna el closure será asignada a la variable getMonth, la cual siempre recordará el entorno donde ésta fue creada (su lexical scope).

>:D El truco para que la función externa sea inmediatamente ejecutada, es encerrar la definición de la función entre paréntesis, de este modo indicamos que se va a ejecutar una function expression.

A tener en cuenta

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

⭐ Lectura recomendada: How to write low garbage real-time Javascript.

Tipos de closure

El primer tipo de closure ya lo vimos en el ejemplo anterior, y es el IIFE, en donde la función externa se invoca inmediatamente para crear el closure.

Closure con parámetros

💡 Vamos a crear una función que permita rellenar con caracteres a la izquierda el texto proporcionado. Podemos configurar los caracteres de relleno y la cantidad total de caracteres que retornará.

function paddingLeft (quantity, fillchar) {
    // default parameters
    quantity = quantity || 3; 
    fillchar = fillchar || "0";

    return function (text) {
        // crea un array del tamaño especificado
        // y lo rellena con el caracter especificado
        // y concatena a la derecha el texto original.
        var filled = new Array(quantity).join(fillchar) + text;
        // retorna la cadena original
        // con el relleno de caracteres a la izquierda
        // y del tamaño especificado.
        return filled.slice(-quantity);
    };
}

var zeroLeftPadding = paddingLeft(); // default parameters
console.log(zeroLeftPadding(8)); // "008"

var dotLeftPadding = paddingLeft(20, ".");
console.log( dotLeftPadding("pg. 13") );
console.log( dotLeftPadding("pg. 18") );

💡 En este caso, tenemos un closure en donde la función externa recibe unos argumentos de configuración y retorna una nueva función cada vez que es invocada.
La función interna retornada, recuerda los argumentos enviados a la función padre.

La diferencia de este closure con el IIFE, está en que el IIFE sólo se ejecuta una vez, mientras que en este caso, la función padre paddingLeft() se puede ejecutar muchas veces, con diferentes argumentos, y cada vez que se ejecuta retorna una nueva función que recuerda los argumentos con que se configuró la función padre.

Código closure
Closure scopes

Redefinir la función

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

function logInit(name) {

    // esta variable solo se incializa una vez
    var _time = new Date();

    // Lazy Function Definition Pattern,
    // se refedine la función original.
    logInit = function(name) {
        console.log("User: " + name + " >");
        console.log("Started at: ", _time);
        return _time;
    }

    // invoca la función redefinida,
    // ésto crea el closure.
    return logInit(name);
}

logInit("Pepe Grillo");
setTimeout(function() {
    logInit("Bibidi Babidi Bu");
}, 2000);

😮 Este caso es interesante. Tenemos la función logInit() la cual, se redefine así misma, y finalmente se invoca así misma. Ésto permite que cuando la función se llame así misma, ejecute el código que la redefine, creando el closure que mantendrá un estado interno privado para la variable _time, de modo que en las siguientes llamadas siempre se ejecutará el código de la función redefinida. Ésta técnica se llama Lazy Function Definition Pattern.

➡ Es muy similar a lo que hace un IIFE, pero con una diferencia, el closure IIFE se invoca inmediatamente, mientras que Lazy Function Definition sólo crea el closure On-Demand, la primera vez que la función es invocada.

En nuestro caso, el closure sólo se crea con la llamada logInit("Pepe Grillo"); aquí se inicializa la variable _time, se redefine la función original, y se invoca ya redefinida, creando el closure, de modo que cuando se hace la segunda llamada logInit("Bibidi Babidi Bu") se ejecuta el código de la nueva función.

⭐ La técnica Lazy Function Definition es buena, sin embargo tiene una gran desventaja, y es que al ser redefinida la función original, ésta pierde transparencia referencial, lo que significa que no se puede confiar en la integridad de dicha función si es asignada a otra variable, enviada como argumento a otra función, o asignada como propiedad de un objeto. Douglas Crockford y otros autores más puristas con la programación funcional no están de acuerdo con ésta técnica, porque una función redefinida pierde su privilegio de first-class citizen. Vea más detalles, pros y contras de este polémico patrón en Lazy Function Definition Pattern.

Conclusión

En este artículo aprendimos a utilizar uno de los pilares de la programación avanzada en JavaScript, los closures. Es 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 caché dentro del closure. Es esencial e importante comprenderlos y aplicarlos para mejorar nuestro nivel de JavaScript.

➡ Recomiendo leer los siguientes artículos:

😎 Happy coding!

15 thoughts on “Closures en JavaScript

    • 😛 jajaja, typo. La cache nos sirve para almacenar objetos en memoria, y de esta forma accederlos mas rapido. Por ejemplo, en vez de crear un objecto cada vez que se invoca una función, se puede crear el objeto en el scope en donde esta definida la función, de modo que la función solo llama al objeto en vez de crearlo en cada llamado.

      Ventajas de la caché: rapidez, performance, menor acceso al Garbage Collector.
      Desventajas: usarlo de forma incorrecta puede utilizar más memoria innecesariamente.

  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