16 marzo, 2008

Clausuras javascript para torpes

Me he permitido traducir el interesante artículo de Morris Johns sobre clausuras ( JavaScript Closures for Dummies | Developing thoughts > Morris Johns ) :

JavaScript Closures (Clausuras) para torpes

Las clausuras no son mágicas

Esta página explica las clausuras de forma que un programador pueda entenderlas - usando código javascript funcional. No es para gurus ni para programadores funcionales. Las clausuras no son difíciles de comprender una vez que se intuye el concepto clave. De cualquier forma, son imposibles de comprender leyendo sobre ellos en publicaciones académicas o en información orientada a la educación. Este artículo está dirigido a programadores con alguna experiencia en un lenguaje de uso común y que puedan leer la siguiente función javascript:
function decirHola(nombre) {
 var texto = 'Hola ' + nombre;
 var decirAlerta = function() { alert(texto); }
 decirAlerta();
}

Un ejemplo de una clausura

Dos resúmenes de una frase:
  • Una clausura son las variables locales de una función - mantenidas vivas después de que la función haya retornado, o también
  • una clausura es una estructura de pila que no se desasigna cuando la función retorna (como si una estructura de pila se asignara en lugar de permanecer en la pila principal).
El siguiente código devuelve una referencia a la función:
function decirHola2(nombre) {
 var texto = 'Hola ' + nombre; // local variable
 var decirAlerta = function() { alert(texto); }
 return decirAlerta;
}


La mayoría de programadores en javascript comprenderán como una referencia a una función es devuelta a una variable en el código anterior. Si no, entonces necesitas entenderlo antes de aprender clausuras. Un programador C podría asociarlo a una función que devuelve un puntero a una función, y que las variables decirAlerta y decir2 fuesen punteros a función. Hay una diferencia crítica entre un puntero en C a una función, y una referencia javascript a una función. En javascript, puedes considerar una variable de referencia a una función como que tiene ambas cosas: un puntero a una función y también un puntero oculto a una clausura. El código anterior tiene una clausura porque la función anónima function() { alert (text); } se declara dentro de otra función, decirHola2() en este ejemplo. En javascript, si usas la palabra clave function dentro de otra función, estás creando una clausura. En C, y en la mayoría de los lenguajes de uso común, cuando una función retorna, todas las variables locales dejan de estar accesibles porque la estructura de pila es destruida. En javascript, si declaras una función dentro de otra función, las variables locales pueden permaneces acceibles después de retornar de la función llamada. Esto se demuestra arriba, porque llamamos a la función say2() después de haber retornado de decirHola2(). Fíjate que el código que llamamos referencia a la variable texto, la cual era una variable local de la función decirHola2().
function() { alert(texto); }


Pulsa el botón de arriba para conseguir que javascript muestre el código de la función anónima. Puedes ver que el código hace referencia a la variable texto. La función anónima puede referenciar texto que mantiene el valor 'Rut' porque las variables locales de decirHola2() se mantienen en la clausura. La magia es que en javascript una referencia a una función también mantiene una referencia secreta a la clausura en la que fue creada - lo que es similar a cómo 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 realmente difíciles de comprender cuando lees sobre ellas, pero cuando ves ejemplos con los que puedes interactuar para ver cómo funcionan, parecen más fáciles. Recomiendo seguir los ejemplos cuidadosamente hasta comprender cómo funcionan. Si empiezas a usar clausuras sin comprender totalmente cómo funcionan, pronto crearás errores muy extraños y difíciles de depurar.

Ejemplo 3

Este ejemplo muestra que las variables locales no se copian (se mantienen por referencia). Es cómo mantener una estructura de pila en memoria cuando la función retorna.
function decir667() {
 // Variable local que termina en la clausura
 var num = 666;
 var decirAlerta = function() { alert(num); }
 num++;
 return decirAlerta;
}

Ejemplo 4

Las tres funciones globales tienen una referencia común a la misma clausura porque las tres están declaradas dentro de una misma llamada a estableceUnasGlobales().
function estableceUnasGlobales() {
 // variable global que termina dentro de la clausura
 var num = 666;
 // almacena algunas referencias a funciones como variables globales
 gAlertaNumero = function() { alert(num); }
 gIncrementaNumero = function() { num++; }
 gPonNumero = function(x) { num = x; }
}


Las tres funciones tienen acceso compartido a la misma clausura - las variables locales de estableceUnasGlobales() donde las tres funciones se han definido. Ten en cuenta que en el ejemplo anterior, si pulsas estableceUnasGlobales() de nuevo, entonces una nueva clausura (estructura de pila) se creará. Las viejas variables gAlertaNumero, gIncrementaNumero, gPonNumero son sobreescritas con nuevas> funciones que tienen una nueva clausura. (En javascript, cuando declaras una función dentro de otra, la(s) funcion(es) interior(es) es/son recreadas de nuevo cada vez la función externa es llamada).

Ejemplo 5

Este es una verdadera fuente de fallos para mucha gente, así que necesitas entenderlo. Ten mucho cuidado si estás definiendo una función dentro de un bucle: las variables locales de la clausura podrían no actuar como podrías pensar en principio.
function construyeLista(lista) {
 var resultado = [];
 for (var i = 0; i < lista.length; i++) {
  var item = 'item' + lista[i];
  resultado.push( function() {alert(item + ' ' + lista[i])} );
 }
 return resultado;
}
function testLista() {
 var fnlista = construyeLista([1,2,3]);
 // uso j sólo para prevenir confusión con i    
 for (var j = 0; j < fnlista.length; j++) {
  fnlista[j]();
 }
}


La línea resultado.push( function() {alert(item + ' ' + lista[i])} añade una referencia a una función anónima tres veces al array resultado. Si no estas familiarizado con las funciones anónimas, considéralas como:
puntero = function() { alert(item + ' ' + lista[i]) };
resultado.push(puntero);
Ten en cuenta que cuando ejecutas el ejemplo, aparece la alerta "item3 undefined" tres veces. Eso es porque, igual que en los ejemplos previos, hay sólo una clausura para las variables locales de construirLista. Cuando las funciones anónimas son llamadas en la línea fnlista[j](); todas usan la misma clausura, y usan el valor actual de i e item dentro de esa clausura (donde i tiene un valor de 3 porque el bucle se ha completado, e item tiene el valor de 'item3').

Ejemplo 6

Este ejemplo muestra que la clausura contiene cualquier variable local que fuese definida en la función externa antes de retornar. Ten en cuenta que la variable alicia se declara realmente después de la función anónima. La función anónima es declarada primero: y cuando esa función es llamada, puede acceder a la variable alicia porque alicia está en la clausura. También decirAlicia()(); directamente llama a la referencia de la función devuelta de decirAlicia() - que es justo lo mismo que fue hecho previamente, pero sin la variable temporal.
function decirAlicia() {
 var decirAlerta = function() { alert(alicia); }
 // variable local que termina estando dentro de la clausura
 var alicia = 'Hola Alicia';
 return decirAlerta;
}



Cuidado: ten en cuenta también que la variable decirAlerta está dentro de la clausura, y que podría ser accedida por cualquier otra función declarada dentro de decirAlicia() o podría ser accedido recursivamente desde la función de dentro.

Ejemplo 7

Este ejemplo final muestra que cada llamada crea una clausura separada para las variables locales. No hay una única clausura para la declaración de función. Hay una clausura para cada llamada a una función.
function nuevaClausura(algunNum, algunaRef) {
 // variables locales que terminan dentro de la clausura
 var num = algunNum;
 var unArray = [1,2,3];
 var ref = algunaRef;
 return function(x) {
  num += x;
  unArray.push(num);
  alert('num: ' + num + 
  '\nunArray ' + unArray.toString() + 
  '\nref.algunaVar ' + ref.algunaVar);
 }
}

Resumen

Si todo parece completamente liado, lo mejor es jugar con los ejemplos. Leer una explicación es mucho más difícil que entender los ejemplos. Mis explicaciones de las clausuras y estructuras de pila no son técnicamente correctas - son simplificaciones burdas dirigidas a ayudar a la comprensión. Una vez se intuye la idea básica, puedes pararte en los detalles después. Puntos finales:
  • Cuando se use function dentro de otra función, se usa una clausura.
  • Cuando se use eval() dentro de una función, se usa una clausura. El texto que evalúas puede referenciar variables locales de una función, y dentro del eval puedes incluso crear nuevas variables locales usando eval('var foo= ...
  • Cuando uses Function() dentro de una función, no se crea una clausura. (La nueva función no puede referenciar variables locales de la función llamando Function() ).
  • Una clausura en javascript es como mantener copia de todas las variables locales, justo como estaban cuando la función retornó.
  • Es mejor pensar que una clausura se crea siempre en el momento de la entrada a la función, y que las variables locales se añaden a esa clausura.
  • Un nuevo conjunto de variables locales se mantiene cada vez que una función con una clausura es llamada (Dado que las funciones contienen una declaración de función en su interior, una referencia a esa función interior es retornada o bien se mantiene una referencia externa de alguna manera).
  • Dos funciones podrían parecer que tienen el mismo código pero podrían tener un comportamiento totalmente distinto debido a su clausura 'oculta'. No creo que el código javascript pueda realmente averiguar si una referencia a función tiene una clausura o no.
  • Si estás intentando hacer cualquier modificación de código fuente ( por ejemplo: miFuncion = Function( miFuncion.toString().replace(/Hola/,'Buenas')); ), no funcionará si miFuncion es una clausura (Desde luego, nunca nunca pensarías en hacer sustituciones de cadenas sobre código fuente, pero ... ).
  • Es posible obtener declaraciones de funciones dentro de declaraciones de funciones dentro de funciones - y puedes obtener clausuras a más de un nivel.
  • Creo que normalmente una clausura es la definición usada para ambos, la función y las variables que son capturadas. Ten en cuenta que no uso esa definición en este artículo.
  • Sospecho que las clausuras de javascript difieren de las que se encuentran normalmente en los lenguajes funcionales.

Enlaces

  • TrimBreakpoint hace un uso rebuscado de las clausuras para dejarte inspeccionar las variables locales de una función desde una ventana emergente de breakpoint (punto de ruptura de la ejecución).
  • Douglas Crockford ha simulado atributos y métodos privados para objetos usando clausuras.
  • Una gran explicación de cómo las clausuras pueden causar fugas de memoria en IE si no tienes cuidado. Al menos con las versiones anteriores a la 7.

Gracias

Si has aprendido clausuras (aquí o en cualquier otro lugar), entonces estoy interesado en cualquier respuesta tuya sobre cualquier cambio que podrías sugerir que hicies este artículo más claro. Envia un email a morrisjohns.com ( morris_closure@ delante). Por favor, ten en cuenta que no soy un guru de javascript ni de las clausuras. Gracias por leerme.

7 comentarios:

Anónimo dijo...

Muchas gracias por traducir el artículo. Facilita mucho su lectura a los nativos de ésta lengua :)

Anónimo dijo...

genio!

Anónimo dijo...

buen articulo man!

Anónimo dijo...

El articulo es bueno, solo que te falta corregir algunas descripciones de los botones p.e. en el 1er ejemplo decirAlerta('Al'); va decirHola('Al'); y cosillas así

me sirvió mucho la segunda vez que lo leí, jeje a la primera no entendía algunas cosas.

saludos!

Anónimo dijo...

Muchas gracias dedicar su tiempo para explicar con su propio conocimiento sobre el tema. ami en particular me aclaro mucho. Aunque quedo con un poquito de dudas que se iran resolviendo con la practica y un poco mas de teoria.

Muchas gracias desde Venezuela

coco dijo...

en realidad la manera como lo explicas es muy diferente a la clausura que conozco en ruby y lisp, lo que me creo mas dudas, un enlace que me ayudo es el siguiente:

http://www.jasoft.org/blog/PermaLink,guid,559517d8-9579-41bd-9e2c-36569dce2681.aspx

vale la pena leerlo porque es muy concreto (aunque no exprime la potencialidad de las clausuras pero ayuda aentenderlas)....

Anónimo dijo...

Wow Javascript tiene poderes que nunca imagine, que clase de brujeria oscura uso el que lo creo ? o.O

Publicar un comentario en la entrada

Últimos links en indiza.com