“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).
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.
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:
- Javascript’s lexical scope, hoisting and closures without mystery por Nick Balestra.
- Closures en JavaScript: entiéndelos de una vez por todas por Óscar Sotorrío.
- JavaScript Closures de MDN.
😎 Happy coding!
El articulo fue re-escrito en algunas secciones, y se agregaron nuevos segmentos para aclarar algunos temas.
Nuevamente gracias!!! Muy buen y entendible articulo.
pero que es el chache? jjaja
😛 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.
que buen articulo!! sos grande saludos!
Gracias Rodrigo, pronto empezaré con algunos articulos de ES2015
Muy bueno !
Que les puedo decir al respecto… nada, muy buena explicación,completa y con los términos correctos, en serio muchas gracias, me sirvió para consolidar conocimientos, sigan así.
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étodoslice
, para que tome los caracteres desde el final de la cadena (desde la derecha) por ejemplo:Gracias por la información, me ha sido muy útil para dar el paso a la programación avanzada con javascript.