Esta página ha sido traducida del inglés por la comunidad.Aprende más y únete a la comunidad de MDN Web Docs.
Closures
Unclosure es la combinación de una función agrupada (dentro de otra) con referencias a su estado adyacente (elentorno léxico). En otras palabras, unclosure te da acceso al alcance de una función externa desde una función interna. En JavaScript, losclosure se crean cada vez que se crea una función, en el momento de la creación de la función.
In this article
Ámbito léxico
Considere el siguiente ejemplo:
function init() { var name = "Mozilla"; // name es una variable local creada por init function displayName() { // displayName() es la función interna que forma el closure console.log(name); // usar la variable declarada en la función padre } displayName();}init();init() crea una variable local llamadaname y una función llamadadisplayName(). La funcióndisplayName() es una función interna que se define dentro deinit() y está disponible solo dentro del cuerpo de la funcióninit(). Tenga en cuenta que la funcióndisplayName() no tiene variables locales propias. Sin embargo, dado que las funciones internas tienen acceso a las variables de las funciones externas,displayName() puede acceder a la variablename declarada en la función principal,init().
Ejecute el código utilizandoeste enlace de JSFiddle y observe que la instrucciónconsole.log() dentro de la funcióndisplayName() muestra con éxito el valor de la variablename, que se declara en su función principal. Este es un ejemplo deámbito léxico, que describe cómo un analizador resuelve los nombres de variables cuando las funciones están anidadas. La palabraléxico se refiere al hecho de que el ámbito léxico utiliza la ubicación donde se declara una variable dentro del código fuente para determinar dónde está disponible esa variable. Las funciones anidadas tienen acceso a variables declaradas en su ámbito externo.
En este ejemplo particular, el ámbito se denominaámbito de función oalcance de la función, porque la variable es accesible y solo es accesible dentro del cuerpo de la función donde se declara.
Alcance con let y const
Tradicionalmente (antes de ES6), JavaScript solo tenía dos tipos de ámbitos:ámbito de la función yámbito global. Las variables declaradas convar tienen un alcance de función o un alcance global, dependiendo de si se declaran dentro de una función o fuera de una función. Esto puede ser complicado, porque los bloques con llaves rizadas no crean ámbitos:
if (Math.random() > 0.5) { var x = 1;} else { var x = 2;}console.log(x);Para personas de otros lenguajes (por ejemplo, C, Java) donde los bloques crean ámbitos, el código anterior debería arrojar un error en la líneaconsole.log, porque estamos fuera del alcance dex en cualquiera de los bloques. Sin embargo, debido a que los bloques no crean ámbitos paravar, las instruccionesvar aquí en realidad crean una variable global. También se presenta a continuaciónun ejemplo práctico que ilustra cómo esto puede causar errores reales cuando se combina conclosures.
En ES6, JavaScript introdujo las declaracioneslet yconst, que, entre otras cosas, comozonas muertas temporales, le permiten crear variables de alcance de bloque.
if (Math.random() > 0.5) { const x = 1;} else { const x = 2;}console.log(x); //ReferenceError: x no está definidoEn esencia, los bloques finalmente se tratan como ámbitos en ES6, pero solo si declaras variables conlet oconst. Además, ES6 introdujomódulos, que introdujo otro tipo de alcance. Losclosure son capaces de capturar variables en todos estos ámbitos, que introduciremos más adelante.
Closure
Considere el siguiente ejemplo:
function makeFunc() { const name = "Mozilla"; function displayName() { console.log(name); } return displayName;}const myFunc = makeFunc();myFunc();Ejecutar este código tiene exactamente el mismo efecto que el ejemplo anterior de la funcióninit() anterior. Lo que es diferente (e interesante) es que la función internadisplayName() se devuelve desde la función externaantes de ejecutarse.
A primera vista, puede parecer poco intuitivo que este código siga funcionando. En algunos lenguajes de programación, las variables locales dentro de una función existen solo durante la ejecución de esa función. Una vez quemakeFunc() termine de ejecutarse, es de esperar que la variablename ya no sea accesible. Sin embargo, debido a que el código sigue funcionando como se esperaba, este obviamente no es el caso en JavaScript.
La razón es que las funciones en JavaScript formanclosures. Unclosure es la combinación de una función y el entorno léxico dentro del cual se declaró esa función. Este entorno consiste en cualquier variable local que estuviera dentro del alcance en el momento en que se creó elclosure. En este caso,myFunc es una referencia a la instancia de la funcióndisplayName que se crea cuando se ejecutamakeFunc. La instancia dedisplayName mantiene una referencia a su entorno léxico, dentro del cual existe la variablename. Por esta razón, cuando se invocamyFunc, la variablename permanece disponible para su uso, y 'Mozilla' se pasa aconsole.log.
Aquí hay un ejemplo un poco más interesante: una funciónmakeAdder:
function makeAdder(x) { return function (y) { return x + y; };}const add5 = makeAdder(5);const add10 = makeAdder(10);console.log(add5(2)); // 7console.log(add10(2)); // 12En este ejemplo, hemos definido una funciónmakeAdder(x), que toma un solo argumentox y devuelve una nueva función. La función que devuelve toma un solo argumentoy y devuelve la suma de la variablex y la variabley.
En esencia,makeAdder es una fábrica de funciones. Crea funciones que pueden añadir un valor específico a su argumento. En el ejemplo anterior, la fábrica de funciones crea dos nuevas funciones: una que suma cinco a su argumento y otra que suma 10.
add5 yadd10 formanclosures. Comparten la misma definición de cuerpo de función, pero almacenan diferentes entornos léxicos. En el entorno léxico deadd5,x es 5, mientras que en el entorno léxico deadd10,x es 10.
Closure prácticos
Losclosure son útiles porque te permiten asociar datos (el entorno léxico) con una función que opera sobre esos datos. Esto tiene paralelismos obvios con la programación orientada a objetos, donde los objetos le permiten asociar datos (las propiedades del objeto) con uno o más métodos.
En consecuencia, puede usar unclosure en cualquier lugar donde normalmente pueda usar un objeto con un solo método.
Las situaciones en las que es posible que desee hacer esto son particularmente comunes en la web. Gran parte del código escrito en JavaScript parafront-end está basado en eventos. Defina algún comportamiento y luego adjúntelo a un evento activado por el usuario (como un clic o una pulsación de tecla). El código se adjunta como una devolución de llamada (una sola función que se ejecuta en respuesta al evento).
Por ejemplo, supongamos que queremos añadir botones a una página para ajustar el tamaño del texto. Una forma de hacerlo es especificar el tamaño de fuente del elementobody (en píxeles) y luego establecer el tamaño de los otros elementos de la página (como los encabezados) utilizando la unidadem relativa:
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px;}h1 { font-size: 1.5em;}h2 { font-size: 1.2em;}Dichos botones interactivos de tamaño de texto pueden cambiar la propiedadfont-size del elementobody, y los ajustes son recogidos por otros elementos de la página gracias a las unidades relativas.
Aquí está el código #"size-12").onclick = size12;document.getElementById("size-14").onclick = size14;document.getElementById("size-16").onclick = size16;
<button>12</button><button>14</button><button>16</button>Ejecuta el código usandoJSFiddle.
Emular métodos privados conclosures
Los lenguajes como Java le permiten declarar métodos como privados, lo que significa que solo pueden ser llamados por otros métodos de la misma clase.
JavaScript, antes declases, no tenía una forma nativa de declararmétodos privados, pero era posible emular métodos privados usandoclosures. Los métodos privados no solo son útiles para restringir el acceso al código. También proporcionan una forma poderosa de gestionar su espacio de nombres global.
El siguiente código ilustra cómo usarclosures para definir funciones públicas que pueden acceder a funciones y variables privadas. Tenga en cuenta que estosclosures siguen elPatrón de diseño de módulo.
const counter = (function () { let privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment() { changeBy(1); }, decrement() { changeBy(-1); }, value() { return privateCounter; }, };})();console.log(counter.value()); // 0.counter.increment();counter.increment();console.log(counter.value()); // 2.counter.decrement();console.log(counter.value()); // 1.En ejemplos anteriores, cadaclosure tenía su propio entorno léxico. Aquí, sin embargo, hay un único entorno léxico que es compartido por las tres funciones:counter.increment,counter.decrement ycounter.value.
El entorno léxico compartido se crea en el cuerpo de una función anónima,que se ejecuta tan pronto como se ha definido (también conocida comoIIFE). El entorno léxico contiene dos elementos privados: una variable llamadaprivateCounter y una función llamadachangeBy. No puedes acceder a ninguno de estos miembros privados desde fuera de la función anónima. En su lugar, puede acceder a ellos utilizando las tres funciones públicas que se devuelven desde el contenedor anónimo.
Esas tres funciones públicas formanclosures que comparten un mismo entorno léxico. Gracias al alcance léxico de JavaScript, cada uno tiene acceso a la variableprivateCounter y a la funciónchangeBy.
const makeCounter = function () { let privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment() { changeBy(1); }, decrement() { changeBy(-1); }, value() { return privateCounter; }, };};const counter1 = makeCounter();const counter2 = makeCounter();console.log(counter1.value()); // 0.counter1.increment();counter1.increment();console.log(counter1.value()); // 2.counter1.decrement();console.log(counter1.value()); // 1.console.log(counter2.value()); // 0.Observa cómo los dos contadores mantienen su independencia el uno del otro. Cadaclosure hace referencia a una versión diferente de la variableprivateCounter a través de su propioclosure. Cada vez que se llama a uno de los contadores, su entorno léxico cambia cambiando el valor de esta variable. Los cambios en el valor de la variable en unclosure no afectan el valor en el otroclosure.
Nota:El uso declosures de esta manera proporciona beneficios que normalmente se asocian con la programación orientada a objetos. En particular,ocultación de datos yencapsulación.
Cadena de alcance de closure
Cadaclosure tiene tres alcances:
- Alcance local (Ámbito propio)
- Alcance envolvente (puede ser alcance de bloque, función o módulo)
- Alcance global
Un error común es no darse cuenta de que en el caso de que la función externa sea en sí misma una función anidada, el acceso al alcance de la función externa incluye el alcance circundante de la función externa, creando efectivamente una cadena de alcances de función. Para demostrarlo, considere el siguiente código de ejemplo.
// ámbito globalconst e = 10;function sum(a) { return function (b) { return function (c) { // ámbito de funciones exteriores return function (d) { // ámbito local return a + b + c + d + e; }; }; };}console.log(sum(1)(2)(3)(4)); // 20También puede escribir sin funciones anónimas:
// ámbito globalconst e = 10;function sum(a) { return function sum2(b) { return function sum3(c) { // ámbito de funciones exteriores return function sum4(d) { // ámbito local return a + b + c + d + e; }; }; };}const sum2 = sum(1);const sum3 = sum2(2);const sum4 = sum3(3);const result = sum4(4);console.log(result); // 20En el ejemplo anterior, hay una serie de funciones anidadas, todas las cuales tienen acceso al ámbito de las funciones externas. En este contexto, podemos decir que losclosures tienen acceso atodos los ámbitos de función externos.
Losclosure también pueden capturar variables en ámbitos de bloque y ámbitos de módulo. Por ejemplo, lo siguiente crea unclosure sobre la variable de ámbito de bloquey:
function outer() { let getY; { const y = 6; getY = () => y; } console.log(typeof y); // undefined console.log(getY()); // 6}outer();Losclosure sobre los módulos pueden ser más interesantes.
// myModule.jslet x = 5;export const getX = () => x;export const setX = (val) => { x = val;};Aquí, el módulo exporta un par de funcionesgetter-setter (para asignar y obtener), que se cierran sobre la variable de alcance del módulox. Incluso cuandox no es directamente accesible desde otros módulos, se puede leer y escribir con las funciones.
import { getX, setX } from "./myModule.js";console.log(getX()); // 5setX(6);console.log(getX()); // 6Losclosure también pueden cerrar sobre valores importados, que se consideran comoenlazadas en vivo, porque cuando el valor original cambia, el importado cambia en consecuencia.
// myModule.jsexport let x = 1;export const setX = (val) => { x = val;};// closureCreator.jsimport { x } from "./myModule.js";export const getX = () => x; // Cerrar sobre un enlace en vivo importadoimport { getX } from "./closureCreator.js";import { setX } from "./myModule.js";console.log(getX()); // 1setX(2);console.log(getX()); // 2Crear closures en bucles: un error común
Antes de la introducción de la palabra clavelet, se producía un problema común con losclosure cuando los creabas dentro de un bucle. Para demostrarlo, considere el siguiente código de ejemplo.
<p>Aquí aparecerán notas útiles</p><p>Correo electrónico: <input type="text" name="email" /></p><p>Nombre: <input type="text" name="name" /></p><p>Edad: <input type="text" name="age" /></p>function showHelp(help) { document.getElementById("help").textContent = help;}function setupHelp() { var helpText = [ { id: "email", help: "Tu dirección de correo electrónico" }, { id: "name", help: "Tu nombre completo" }, { id: "age", help: "Tu edad (debes ser mayor de 16 años)" }, ]; for (var i = 0; i < helpText.length; i++) { // La razón es el uso de `var` en esta línea var item = helpText[i]; document.getElementById(item.id).onfocus = function () { showHelp(item.help); }; }}setupHelp();Intenta ejecutar el código enJSFiddle.
La matrizhelpText define tres consejos útiles, cada uno asociado con el ID de un campo de entrada en el documento. El bucle recorre estas definiciones, conectando un eventoonfocus a cada uno que muestra el método de ayuda asociado.
Si pruebas este código, verás que no funciona como esperabas. No importa en qué campo se centre, se mostrará el mensaje sobre su edad.
La razón de esto es que las funciones asignadas aonfocus formanclosures; consisten en la definición de la función y el entorno capturado desde el alcance de la funciónsetupHelp. El bucle ha creado tresclosure, pero cada uno comparte el mismo entorno léxico único, que tiene una variable con valores cambiantes (item). Esto se debe a que la variableitem se declara convar y, por lo tanto, tiene un alcance de función debido a la elevación. El valor deitem.help' se determina cuando se ejecutan las devoluciones de llamadaonfocus. Debido a que el bucle ya ha seguido su curso en ese momento, el objeto variableitem(compartido por los tres _closure_) se ha dejado apuntando a la última entrada en la listahelpText`.
Una solución en este caso es usar másclosure: en particular, usar una fábrica de funciones como se describió anteriormente:
function showHelp(help) { document.getElementById("help").textContent = help;}function makeHelpCallback(help) { return function () { showHelp(help); };}function setupHelp() { var helpText = [ { id: "email", help: "Tu dirección de correo electrónico" }, { id: "name", help: "Tu nombre completo" }, { id: "age", help: "Tu edad (debes ser mayor de 16 años)" }, ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); }}setupHelp();Ejecuta el código usandoeste enlace de JSFiddle.
Esto funciona como se esperaba. En lugar de que todas las devoluciones de llamada compartan un único entorno léxico, la funciónmakeHelpCallback creaun nuevo entorno léxico para cada devolución de llamada, en el quehelp se refiere a la cadena correspondiente de la matrizhelpText.
Otra forma de escribir lo anterior utilizando cierres anónimos es:
function showHelp(help) { document.getElementById("help").textContent = help;}function setupHelp() { var helpText = [ { id: "email", help: "Tu dirección de correo electrónico" }, { id: "name", help: "Tu nombre completo" }, { id: "age", help: "Tu edad (debes ser mayor de 16 años)" }, ]; for (var i = 0; i < helpText.length; i++) { (function () { var item = helpText[i]; document.getElementById(item.id).onfocus = function () { showHelp(item.help); }; })(); //Adjunto del detector de eventos inmediatos con el valor actual del elemento (conservado hasta la iteración). }}setupHelp();Si no desea utilizar másclosure, puede utilizar la palabra clavelet oconst:
function showHelp(help) { document.getElementById("help").textContent = help;}function setupHelp() { const helpText = [ { id: "email", help: "Tu dirección de correo electrónico" }, { id: "name", help: "Tu nombre completo" }, { id: "age", help: "Tu edad (debes ser mayor de 16 años)" }, ]; for (let i = 0; i < helpText.length; i++) { const item = helpText[i]; document.getElementById(item.id).onfocus = () => { showHelp(item.help); }; }}setupHelp();Este ejemplo usaconst en lugar devar, por lo que cadaclosure vincula la variable de alcance de bloque, lo que significa que no se requierenclosure adicionales.
Otra alternativa podría ser usarforEach() para iterar sobre la matrizhelpText y adjuntar un detector a cada<input>, como se muestra:
function showHelp(help) { document.getElementById("help").textContent = help;}function setupHelp() { var helpText = [ { id: "email", help: "Tu dirección de correo electrónico" }, { id: "name", help: "Tu nombre completo" }, { id: "age", help: "Tu edad (debes ser mayor de 16 años)" }, ]; helpText.forEach(function (text) { document.getElementById(text.id).onfocus = function () { showHelp(text.help); }; });}setupHelp();Consideraciones de rendimiento
Como se mencionó anteriormente, cada instancia de función gestiona su propio alcance y cierre. Por lo tanto, no es prudente crear funciones innecesariamente dentro de otras funciones si no se necesitanclosures para una tarea en particular, ya que afectará negativamente el rendimiento del script tanto en términos de velocidad de procesamiento como de consumo de memoria.
Por ejemplo, al crear un nuevo objeto/clase, los métodos normalmente deben asociarse al prototipo del objeto en lugar de definirse en el constructor del objeto. La razón es que cada vez que se llama al constructor, los métodos se reasignan (es decir, para cada creación de objetos).
Observemos el siguiente caso:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function () { return this.name; }; this.getMessage = function () { return this.message; };}Debido a que el código anterior no aprovecha los beneficios de usarclosures en esta instancia en particular, podríamos reescribirlo para evitar usarclosures de la siguiente manera:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString();}MyObject.prototype = { getName() { return this.name; }, getMessage() { return this.message; },};Sin embargo, no se recomienda redefinir el prototipo. En su lugar, el siguiente ejemplo se adjunta al prototipo existente:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString();}MyObject.prototype.getName = function () { return this.name;};MyObject.prototype.getMessage = function () { return this.message;};En los dos ejemplos anteriores, el prototipo heredado puede ser compartido por todos los objetos y las definiciones del método no necesitan ocurrir en cada creación de objetos. ConsulteLa herencia y la cadena de prototipos para obtener más información.