Aprovecho este espacio para ofrecer una versión traducida a nuestro idioma de una de las respuestas que refieres en tu pregunta, la cual tiene como base el excelente artículo de Morris sobre Closures titulado JavaScript Closures for Dummies.
No sé si esto pueda ser entendido por un niño de 6 años, pero puede ser al menos un comienzo para público de todas las edades :).
Dado que allí la respuesta ha sido marcada como Wiki de comunidad, respetamos la intención del autor, y la marcamos también aquí como Wiki.
Las clausuras no son mágicas
Esta página explica las clausuras para que un programador pueda entenderlas, utilizando el código JavaScript de trabajo. No es para gurús ni para programadores funcionales.
Las clausuras no son difíciles de entender una vez que el concepto de base se ha desarrollado. Sin embargo, ¡son imposibles de entender al leer cualquier artículo académico o información académica sobre ellas!
Este artículo está dirigido a programadores con cierta experiencia en programación en un lenguaje de flujo principal, y que pueden leer la siguiente función de JavaScript:
function sayHello(name) {
var text = 'Hello ' + name;
var say = function() { console.log(text); }
say();
}
sayHello('Joe');
Dos resúmenes breves
Cuando una función (foo
) declara otras funciones (bar
y baz
), la familia de variables locales creadas en foo
no se destruye cuando la función sale. Las variables simplemente se vuelven invisibles para el mundo exterior. Por lo tanto, foo
puede devolver astutamente las funciones bar
y baz
, y éstas pueden continuar leyendo, escribiendo y comunicándose entre sí a través de esta familia cerrada de variables ("la clausura") con la que nadie más puede entrometerse, ni siquiera alguien que llame a foo
otra vez en el futuro.
Una clausura es una forma de apoyar funciones de primera clase; es una expresión que puede hacer referencia a variables dentro de su alcance (cuando se declaró por primera vez), asignarse a una variable, pasarse como argumento a una función o devolverse como resultado de una función.
Un ejemplo de clausura
El siguiente código devuelve una referencia a una función:
function sayHello2(name) {
var text = 'Hello ' + name; // variable local
var say = function() { console.log(text); }
return say;
}
var say2 = sayHello2('Bob');
say2(); // imprime "Hello Bob"
La mayoría de los programadores de JavaScript entenderán cómo una referencia a una función se devuelve a una variable (say2
) en el código anterior. Si no lo hace, entonces necesita comprender eso antes de que pueda aprender acerca de las clausuras. Un programador que usa C pensaría que la función devolvía un puntero a una función, y que las variables say
y say2
eran cada una un puntero a una función.
Hay una diferencia crítica entre un puntero C a una función y una referencia de JavaScript a una función. En JavaScript, se puede pensar que una variable de referencia de función tiene tanto un puntero a una función como un puntero oculto a una clausura.
El código anterior tiene una clausura porque la función anónima function() { console.log(text); }
se declara dentro de otra función, sayHello2 ()
en este ejemplo. En JavaScript, si utilizas la palabra clave function
dentro de otra función, estás creando una clausura.
En C y en la mayoría de los otros lenguajes comunes, después de que una función retorna, todas las variables locales ya no son accesibles porque se destruye el marco de pila.
En JavaScript, si declaras una función dentro de otra función, las variables locales de la función externa pueden permanecer accesibles después de que la función haya retornado. Esto se demuestró anteriormente, porque llamamos a la función say2 ()
después de haber retornado de sayHello2 ()
. Observa que el código que llamamos hace referencia a la variable text
, que era una variable local de la función sayHello2 ()
.
function() { console.log(text); } // Salida de say2.toString();
Si observamos la salida de say2.toString ()
, podemos ver que el código se refiere a la variable text
. La función anónima puede hacer referencia al texto que contiene el valor 'Hola Bob' porque las variables locales de sayHello2 ()
se han mantenido vivas secretamente en la clausura.
La clave es que en JavaScript una referencia de función también tiene una referencia secreta a la clausura en la que se creó, de manera similar a como los delegados son un puntero a un método más una referencia secreta a un objeto.
Más ejemplos
Por alguna razón, las clausuras parecen ser muy difíciles de entender cuando lees sobre ellas, pero cuando ves algunos ejemplos, queda claro cómo funcionan (me tomó un tiempo). Recomiendo trabajar con los ejemplos cuidadosamente hasta que entiendas cómo funcionan. Si comienzas a usar clausuras sin entender completamente cómo funcionan, ¡pronto crearías algunos errores muy extraños!
Ejemplo 3
Este ejemplo muestra que las variables locales no se copian, se mantienen por referencia. ¡Es como si el marco de pila se mantuviera vivo en la memoria incluso después de que existe la función externa!
function say667() {
// Variable local que termina dentro de la clausura
var num = 42;
var say = function() { console.log(num); }
num++;
return say;
}
var sayNumber = say667();
sayNumber(); // escribe 43
Ejemplo 4
Las tres funciones globales tienen una referencia común al mismo cierre porque todas se declaran dentro de una sola llamada a setupSomeGlobals()
.
var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
// Variable local que termina dentro de la clausura
var num = 42;
// Almacena algunas referencias a funciones como variables globales.
gLogNumber = function() { console.log(num); }
gIncreaseNumber = function() { num++; }
gSetNumber = function(x) { num = x; }
}
setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5
var oldLog = gLogNumber;
setupSomeGlobals();
gLogNumber(); // 42
oldLog() // 5
Las tres funciones tienen acceso compartido a la misma clausura: las variables locales de setupSomeGlobals()
cuando se definieron las tres funciones.
Ten en cuenta que en el ejemplo anterior, si llamas a setupSomeGlobals()
nuevamente, se creará un nuevo cierre (stack-frame!). Las antiguas variables gLogNumber
, gIncreaseNumber
, gSetNumber
se sobrescriben con nuevas funciones que tienen la nueva clausura. (En JavaScript, cada vez que declaras una función dentro de otra función, las funciones internas se recrean nuevamente cada vez que se llama a la función externa).
Ejemplo 5
Este ejemplo muestra que la clausura contiene variables locales que se declararon dentro de la función externa antes de salir. Ten en cuenta que la variable alice
se declara realmente después de la función anónima. La función anónima se declara primero, y cuando se llama a esa función, puedes acceder a la variable alice
porque alice
está en el mismo ámbito (JavaScript hace la elevación de variables). También sayAlice()()
simplemente llama directamente a la referencia de función devuelta por sayAlice()
, es exactamente lo mismo que se hizo anteriormente pero sin la variable temporal.
function sayAlice() {
var say = function() { console.log(alice); }
// Variable local que termina dentro de la clausura.
var alice = 'Hello Alice';
return say;
}
sayAlice()();// Escribe "Hello Alice"
Difícil: también debes tener en cuenta que la variable say
también está dentro de la clausura, y puede accederse a ella mediante cualquier otra función que pueda declararse dentro de sayAlice()
, o puede accederse recursivamente dentro de la función que está dentro.
Ejemplo 6
Este es un verdadero problema para muchas personas, por lo que necesitas entenderlo. Ten mucho cuidado si estás definiendo una función dentro de un bucle: es posible que las variables locales de la clausura no actúen como podrías pensar.
Necesitas entender la característica de "elevación de variables" en Javascript para entender este ejemplo.
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = 'item' + i;
result.push( function() {console.log(item + ' ' + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
// Usando j solo para ayudar a prevenir la confusión -- se podría usar i.
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
testList() //escribe "item2 undefined" 3 veces
La línea result.push (function () {console.log (item + '' + list [i])}
agrega una referencia a una función anónima tres veces en la matriz de resultados. Si no estás tan familiarizado con las funciones anónimas, piense que es como:
pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);
¡Ten en cuenta que cuando ejecutas el ejemplo, "item2 undefined" se registra tres veces! Esto se debe a que, al igual que en los ejemplos anteriores, solo hay una clausura para las variables locales para buildList
(que son resultado, i
y item
). Cuando las funciones anónimas se llaman en la línea fnlist [j]()
; todos utilizan la misma clausura única, y usan el valor actual para i
y para item
dentro de esa clausura (donde i
tiene un valor de 3 porque el bucle se había completado, y item
tiene un valor de 'item2'). Ten en cuenta que estamos indexando desde 0, por lo tanto, item
tiene un valor de item2. Y el i++
incrementará i
al valor 3.
Puede ser útil ver qué sucede cuando se utiliza una declaración a nivel de bloque de la variable item
(a través de la palabra clave let
) en lugar de una declaración de variable con ámbito de función a través de la palabra clave var
. Si se realiza ese cambio, entonces cada función anónima en el resultado de la matriz tiene su propia clausura. Cuando se ejecuta el ejemplo, la salida es la siguiente:
item0 undefined
item1 undefined
item2 undefined
Si la variable i
también se define utilizando let
en lugar de var
, entonces la salida es:
item0 1
item1 2
item2 3
Ejemplo 7
En este ejemplo final, cada llamada a la función principal crea una clausura separada.
function newClosure(someNum, someRef) {
// Variables locales que terminan dentro de la clausura.
var num = someNum;
var anArray = [1,2,3];
var ref = someRef;
return function(x) {
num += x;
anArray.push(num);
console.log('num: ' + num +
'; anArray: ' + anArray.toString() +
'; ref.someVar: ' + ref.someVar + ';');
}
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
Sumario
Si todo parece completamente incierto, entonces lo mejor es jugar con los ejemplos. Leer una explicación es mucho más difícil de entender que los ejemplos. Mis explicaciones de clausuras y apilamientos, etc. no son técnicamente correctas, son simplificaciones generales que pretenden ayudar a comprender. Una vez que la idea básica ha sido asimilada, puedes recoger los detalles más adelante.
Puntos finales:
Cuando se utiliza function
dentro de otra función, se utiliza una clausura.
Siempre que uses eval()
dentro de una función, se usa una clausura. El texto que se evalúa puede hacer referencia a las variables locales de la función, y dentro de eval
incluso puedes crear nuevas variables locales utilizando eval ('var foo = ...')
Cuando utilizas new function (...)
(el constructor de funciones) dentro de una función, no creas una clausura. (La nueva función no puede hacer referencia a las variables locales de la función externa).
Una clausura en JavaScript es como mantener una copia de todas las variables locales, tal como eran cuando una función salía (exit
).
Probablemente es mejor pensar que en una clausura siempre se crea solo una entrada a una función, y las variables locales se agregan a esa clausura.
Se mantiene un nuevo conjunto de variables locales cada vez que se llama a una función con una clausura (dado que la función contiene una declaración de función dentro de ella, y se devuelve una referencia a esa función interna o se mantiene una referencia externa de alguna manera).
Dos funciones pueden parecer que tienen el mismo texto de origen, pero tienen un comportamiento completamente diferente debido a su clausura "oculta". No creo que el código JavaScript pueda realmente averiguar si una referencia de función tiene una clausura o no.
Si estás intentando realizar modificaciones dinámicas al código fuente (por ejemplo: myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));)
, no funcionará si myFunction
es una clausura ( por supuesto, nunca se le ocurriría hacer una sustitución de cadena de código fuente en tiempo de ejecución, pero ...).
Es posible obtener declaraciones de funciones dentro de las declaraciones de funciones dentro de las funciones y &mdash, y puedes obtener clausuras en más de un nivel.
Creo que normalmente una clausura es un término para la función junto con las variables que se capturan. ¡Ten en cuenta que no uso esa definición en este artículo!
Sospecho que las clausuras en JavaScript difieren de las que se encuentran normalmente en los lenguajes funcionales.
Enlaces
http://bonsaiden.github.io/JavaScript-Garden/es/#function.closures
Disfruten programar! – fredyfx Dec 16 '15 at 16:39