Cierres en JavaScript explicados con ejemplos

¿Qué son los cierres?

Un cierre es la combinación de una función y el entorno léxico (alcance) dentro del cual se declaró esa función. Los cierres son una propiedad fundamental y poderosa de Javascript. Este artículo analiza el "cómo" y el "por qué" de los cierres:

Ejemplo

//we have an outer function named walk and an inner function named fly function walk (){ var dist = '1780 feet'; function fly(){ console.log('At '+dist); } return fly; } var flyFunc = walk(); //calling walk returns the fly function which is being assigned to flyFunc //you would expect that once the walk function above is run //you would think that JavaScript has gotten rid of the 'dist' var flyFunc(); //Logs out 'At 1780 feet' //but you still can use the function as above //this is the power of closures

Otro ejemplo

function by(propName) { return function(a, b) { return a[propName] - b[propName]; } } const person1 = {name: 'joe', height: 72}; const person2 = {name: 'rob', height: 70}; const person3 = {name: 'nicholas', height: 66}; const arr_ = [person1, person2, person3]; const arr_sorted = arr_.sort(by('height')); // [ { name: 'nicholas', height: 66 }, { name: 'rob', height: 70 },{ name: 'joe', height: 72 } ]

El cierre 'recuerda' el entorno en el que fue creado. Este entorno consta de las variables locales que estaban dentro del alcance en el momento en que se creó el cierre.

function outside(num) { var rememberedVar = num; // In this example, rememberedVar is the lexical environment that the closure 'remembers' return function inside() { // This is the function which the closure 'remembers' console.log(rememberedVar) } } var remember1 = outside(7); // remember1 is now a closure which contains rememberedVar = 7 in its lexical environment, and //the function 'inside' var remember2 = outside(9); // remember2 is now a closure which contains rememberedVar = 9 in its lexical environment, and //the function 'inside' remember1(); // This now executes the function 'inside' which console.logs(rememberedVar) => 7 remember2(); // This now executes the function 'inside' which console.logs(rememberedVar) => 9 

Los cierres son útiles porque le permiten 'recordar' datos y luego le permiten operar con esos datos a través de funciones devueltas. Esto permite que javascript emule métodos privados que se encuentran en otros lenguajes de programación. Los métodos privados son útiles para restringir el acceso al código, así como para administrar su espacio de nombres global.

Variables y métodos privados

Los cierres también se pueden utilizar para encapsular datos / métodos privados. Echale un vistazo a éste ejemplo:

const bankAccount = (initialBalance) => { const balance = initialBalance; return { getBalance: function() { return balance; }, deposit: function(amount) { balance += amount; return balance; }, }; }; const account = bankAccount(100); account.getBalance(); // 100 account.deposit(10); // 110

En este ejemplo, no podremos acceder balancedesde ningún lugar fuera de la bankAccountfunción, lo que significa que acabamos de crear una variable privada. ¿Dónde está el cierre? Bueno, piensa en lo que bankAccount()está regresando. En realidad, devuelve un objeto con un montón de funciones dentro de él, y sin embargo, cuando llamamos account.getBalance(), la función es capaz de "recordar" su referencia inicial balance. Ese es el poder del cierre, donde una función "recuerda" su alcance léxico (alcance de tiempo de compilación), incluso cuando la función se ejecuta fuera de ese alcance léxico.

Emulando variables de ámbito de bloque.

Javascript no tenía un concepto de variables de ámbito de bloque. Lo que significa que al definir una variable dentro de un forloop, por ejemplo, esta variable también es visible desde fuera del forloop. Entonces, ¿cómo pueden los cierres ayudarnos a resolver este problema? Vamos a ver.

 var funcs = []; for(var i = 0; i < 3; i++){ funcs[i] = function(){ console.log('My value is ' + i); //creating three different functions with different param values. } } for(var j = 0; j < 3; j++){ funcs[j](); // My value is 3 // My value is 3 // My value is 3 }

Dado que la variable i no tiene alcance de bloque, su valor dentro de las tres funciones se actualizó con el contador de bucle y creó valores maliciosos. El cierre puede ayudarnos a resolver este problema creando una instantánea del entorno en el que se encontraba la función cuando se creó, conservando su estado.

 var funcs = []; var createFunction = function(val){ return function() {console.log("My value: " + val);}; } for (var i = 0; i < 3; i++) { funcs[i] = createFunction(i); } for (var j = 0; j < 3; j++) { funcs[j](); // My value is 0 // My value is 1 // My value is 2 }

Las últimas versiones de javascript es6 + tienen una nueva palabra clave llamada let que se puede usar para darle a la variable un ámbito de bloques. También hay muchas funciones (forEach) y bibliotecas completas (lodash.js) que se dedican a resolver problemas como los explicados anteriormente. Sin duda, pueden aumentar su productividad, sin embargo, sigue siendo extremadamente importante tener conocimiento de todos estos problemas al intentar crear algo grande.

Los cierres tienen muchas aplicaciones especiales que son útiles al crear grandes programas de JavaScript.

  1. Emulando variables privadas o encapsulamiento
  2. Hacer llamadas del lado del servidor asincrónico
  3. Creando una variable de ámbito de bloque.

Emulando variables privadas.

A diferencia de muchos otros lenguajes, Javascript no tiene un mecanismo que le permita crear variables de instancia encapsuladas dentro de un objeto. Tener variables de instancia públicas puede causar muchos problemas al crear programas medianos a grandes. Sin embargo, con los cierres, este problema se puede mitigar.

Al igual que en el ejemplo anterior, puede crear funciones que devuelvan literales de objeto con métodos que tienen acceso a las variables locales del objeto sin exponerlas. Por lo tanto, hacerlos efectivamente privados.

Los cierres también pueden ayudarlo a administrar su espacio de nombres global para evitar colisiones con datos compartidos globalmente. Por lo general, todas las variables globales se comparten entre todos los scripts de su proyecto, lo que definitivamente le dará muchos problemas al crear programas de tamaño mediano a grande. Es por eso que los autores de bibliotecas y módulos usan cierres para ocultar los métodos y datos de un módulo completo. Esto se llama patrón de módulo, utiliza una expresión de función invocada inmediatamente que exporta solo cierta funcionalidad al mundo exterior, lo que reduce significativamente la cantidad de referencias globales.

Aquí hay una pequeña muestra del esqueleto de un módulo.

var myModule = (function() = { let privateVariable = 'I am a private variable'; let method1 = function(){ console.log('I am method 1'); }; let method2 = function(){ console.log('I am method 2, ', privateVariable); }; return { method1: method1, method2: method2 } }()); myModule.method1(); // I am method 1 myModule.method2(); // I am method 2, I am a private variable

Los cierres son útiles para capturar nuevas instancias de variables privadas contenidas en el entorno 'recordado', y solo se puede acceder a esas variables a través de la función o los métodos devueltos.

Vectores

Un vector es quizás el tipo de colección más simple en Clojure. Puede pensar en ello como una matriz en Javascript. Definamos un vector simple:

(def a-vector [1 2 3 4 5]) ;; Alternatively, use the vector function: (def another-vector (vector 1 2 3 4 5)) ;; You can use commas to separate items, since Clojure treats them as whitespace. (def comma-vector [1, 2, 3, 4, 5])

Verá que usa corchetes, como una matriz en JS. Dado que Clojure, como JS, se escribe dinámicamente, los vectores pueden contener elementos de cualquier tipo, incluidos otros vectores.

(def mixed-type-vector [1 "foo" :bar ["spam" 22] #"^baz$"])

Agregar elementos a un vector

Puede agregar elementos a un vector usando conj. También puede anteponer a una lista usando into, pero tenga en cuenta que intoestá destinado a fusionar dos vectores, por lo que ambos argumentos deben ser vectores, y usar intoes más lento que usar conj.

(time (conj [1 2] 3)) ; => "Elapsed time: 0.032206 msecs" ; [1 2 3] (time (into [1] [2 3])) ; => "Elapsed time: 0.078499 msecs" ; [1 2 3]
:rocket:

IDEOne it!

Recuperar elementos de un vector

You can retrieve items from a vector using get. This is equivalent to using bracket notation to access items in an array in many imperative languages. Items in a vector are 0-indexed, counting from the left.

var arr = [1, 2, 3, 4, 5]; arr[0]; // => 1

In Clojure, this would be written like so:

(def a-vector [1 2 3 4 5]) (get a-vector 0) ; => 1

You can also give get a default value, if you give it an index that isn’t in the array.

;; the list doesn't have 2147483647 elements, so it'll return a string instead. (get a-vector 2147483646 "sorry, not found!") ; => "sorry, not found!"

Converting other collections into vectors

Non-vector data structures can be converted into vectors using the vec function. With hashmaps, this produces a 2D vector containing pairs of keys and values.

(vec '(1 2 3 4 5)) ; => [1 2 3 4 5] (vec {:jack "black" :barry "white"}) ; => [[:jack "black"] [:barry "white"]]

When to use a vector?

A vector should be used in almost all cases if you need a collection, because they have the shortest random-access times, which makes it easy to retrieve items from a vector. Note that vectors are ordered. If order doesn’t matter, it may be better to use a set. Also note that vectors are designed for appending items; if you need to prepend items, you might want to use a list.

More info on Closures:

  • Learn JavaScript closures in six minutes
  • A basic guide to closures in JavaScript
  • Discover the power of closures in VueJS
  • JavaScript closures explained by mailing a package