Introducción
Ya hemos tratado con anterioridad los patrones de diseño en JavaScript; hemos visto cómo algunas de estas estructuras son idóneas para articular módulos y crear aplicaciones complejas, escalables y fáciles de leer. Sin embargo, casi siempre estos patrones implican un proceso de estudio para ver cómo adecuarlas a nuestros códigos, sobre todo cuando éstos ya se encuentra en un estadio avanzado.
Un caso especial es el patrón que veremos a continuación (más adelante se discutirá si podemos hablar realmente de patrón o no), el cual no requiere de prácticamente ningún esfuerzo para su implementación: resulta muy sencillo de utilizar en cualquier momento de un desarrollo y promete grandes ventajas en cuanto a rendimiento y ahorro de memoria: se trata del nullify o, traduciéndolo al castellano, «tender al nulo«…
La idea principal
Javascript maneja su memoria de un modo un tanto peculiar. Definimos variables a las que asignamos valores, modificamos su contenido e incluso cambiamos su tipo durante el tiempo de ejecución. A medida que se suceden funciones y subrutinas, el sistema (el intérprete), puede necesitar de memoria adicional para guardar nuevos valores u objetos. Es en ese momento cuando entra en acción el famoso recolector de basura cuya finalidad es eliminar de la memoria aquellas variables (y su valor) que ya no necesitamos. El proceso interno que lleva a cada intérprete el evaluar qué variable puede o no borrar varía en cada implementación por lo que puede comprobarse fácilmente cómo, por ejemplo, Chrome (V8 en realidad), purga su memoria de un modo diferente a Firefox (JägerMonkey).
El patrón nullify viene a facilitarle las cosas al recolector de basura indicándole qué variables puede liberar de memoria sin tener que esperar a que el sistema se colapse produciendo esos molestos instantes en los todo parece congelarse. Para ello, la idea general es la de sobreescribir el valor de nuestras variables con null tan pronto como no las necesitemos.
Y, ¿cómo detectar fácilmente variables de las que podemos prescindir? Pues de una forma muy sencilla: acudiendo al final del ámbito local (scope) donde han sido declarada. Esto, dicho de una forma mucho más clara, es yendo al final de cada función en nuestro código y asignar null a todas las variables locales que hayamos empleado en la misma: si una función ha completado su cometido, es decir, ha llegado al final, todas sus variables ya no son necesarias fuera de ella.
Ejemplo
Como este patrón es aplicable a todo tipo de funciones, veamos un ejemplo sencillo:
var myFunction = function(){ var foo = document.getElementById("layer1"), bar = document.getElementById("layer2"), foobar = document.getElementById("layer3"); foo.innerHTML = "Hello World"; bar.innerHTML = "Goodbye Lenin"; foobar.innerHTML = "Another Hello World"; // Nullify Pattern foo = bar = foobar = null; }; |
Si observamos el código anterior, encontramos este patrón en la última instrucción:
foo = bar = foobar = null; |
Ahí hemos asignado null a todas las variables locales que hemos empleado indicándole al recolector de basura que puede liberarlas de memoria de inmediato. Como podemos intuir, es una práctica completamente segura ya que, una vez llegados al final de la función, no es necesario continuar almacenando el valor de sus variables locales. Del mismo modo, podemos ver cómo su implementación es simple y transparente.
Uso en funciones con return
Cuando tratamos con funciones que devuelven un valor, no podemos vaciar sus variables antes de la sentencia return ya que lógicamente alteraríamos el resultado. Tampoco podemos incluir nuestro patrón detrás ya que el intérprete nunca alcanzaría ese código:
var myFunction = function(){ var foo = document.getElementById("layer1"), bar = document.getElementById("layer2"), foobar = document.getElementById("layer3"); return { sample1 : foo.innerHTML sample1 : bar.innerHTML; sample1 : foobar.innerHTML; }; // This code is unreachable: foo = bar = foobar = null; }; |
Una herramienta de validación de código como JSLint o JSHint nos advertirían rápidamente del error anterior: nada por debajo de return es evaluado.
Para subsanar esto, podemos recurrir a la estructura try / finally con la que si podríamos lanzar el return y, justo después, liberar el valor de aquellas variables que hayamos definido:
var myFunction = function(){ var foo = document.getElementById("layer1"), bar = document.getElementById("layer2"), foobar = document.getElementById("layer3"); try { return { sample1 : foo.innerHTML sample1 : bar.innerHTML; sample1 : foobar.innerHTML; }; } finally { foo = bar = foobar = null; } }; |
La poco conocida estructura try / finally permite lanzar una o varias sentencias desde el bloque try para, inmediatamente a continuación, pasar a ejecutar las del bloque finally. Esto nos permite realizar nuestro return y, además, encadenarle el patrón que estamos estudiando. El resultado es que, en esta ocasión, el código si es interpretado por el motor Javascript.
Discusión sobre si podemos considerarlo o no un patrón
Visto el último ejemplo, rápidamente podemos preguntarnos por una cuestión de rendimiento: ¿no es la estructura try / catch / finally muy lenta? Pues si que lo es y es precisamente en ese punto donde debemos preguntarnos sobre si este patrón puede considerarse tal.
La definición de patrón de diseño indica que debe tratarse de una estructura o composición válida para solucionar problemas comunes en el desarrollo de software. Para que una solución sea considerada un patrón debe poseer ciertas características y una de ellas es que debe haber comprobado su efectividad con independencia al escenario en el que se aplique.
Y aquí está la cuestión: try / catch / finally supone una penalización en rendimiento que tiene que sopesarse con la ganancia en la gestión de memoria que supone este patrón. Y ese balance, no siempre tiene que resultar favorable al nullify.
Este patrón funciona excepcionalmente bien cuando se manejan nodos provenientes del DOM. Es más, en esos casos, mantener una referencia a un elemento que puede haber cambiado durante el transcurso de una aplicación o un algoritmo complejo, puede provocar las conocidas fugas de memoria:
function createAndRemove() { // Creating a DIV var newElement = document.createElement('div'); newElement.id = "myDiv"; newElement.style.width = "300px"; newElement.style.height = "300px"; newElement.style.backgroundColor = 'red'; // Inserting the DIV into de Document document.body.appendChild( newElement ); // Removing the DIV var myElement = document.getElementById('myDiv'); document.body.removeChild( myElement ); return true; } |
Para entender porqué el ejemplo anterior supone un coste de memoria innecesario, hay que recordar que el DOM conserva todos sus nodos, incluso si estos han sido borrados de su mismo árbol; la única forma de eliminarlos definitivamente es mediante un refresco de página. Cuando el sistema requiere de más memoria es cuando entra en acción recolector.
En el caso anterior, tanto la variable newElement como myElement, continúan requiriendo de memoria reservada una vez han hecho su trabajo. En este determinado caso, el uso del nullify está plenamente justificado y es recomendable:
function createAndRemove() { // Creating a DIV var newElement = document.createElement('div'); newElement.id = "myDiv"; newElement.style.width = "300px"; newElement.style.height = "300px"; newElement.style.backgroundColor = 'red'; // Inserting the DIV into de Document document.body.appendChild( newElement ); // Removing the DIV var myElement = document.getElementById('myDiv'); document.body.removeChild( myElement ); try { return true; } finally { newElement = myElement = null; } } |
La ganancia en términos de rendimiento de memoria es presumiblemente mayor que el tiempo que el intérprete necesita para procesar el try / finally.
Sin embargo, en otros casos donde las variables únicamente actúen como operadores o contadores, puede que la ganancia no sea tan interesante:
function countArr( arr ){ var result = 0, length = arr.length, index = -1; while( ++index < length ){ result += arr[index]; } return result; } |
En el ejemplo anterior, las variables en juego solo contienen valores enteros, lo que supone apenas unos bits de memoria. En este caso, el uso de un try / finally puede penalizar por encima de la ganancia, por lo que habría que sopesar su uso utilizando alguna herramienta de medición.
Conclusión
El patrón nullify es uno de los más sencillo de implementar en un proyecto y puede suponer una mejora importante en aquellas aplicaciones donde se haga, por ejemplo, un uso pesado del DOM.
Con apenas unas líneas de código muy transparentes, podemos indicar al recolector de basura qué variables ya no nos son necesarias. Sin duda, una práctica recomendable que puede marcar la diferencia y evitae ese molesto efecto de congelado que a veces sufren nuestras aplicaciones o páginas web.
Buenas, Carlos, sólo un comentario sobre lo que comentas.
Añadir el try-finally hace algo más lento el código, pero no tanto como si lo que utilizaramos fuera el try-catch-finally.
En nuestro caso el try-finally es tanto más lento como puede suponer el ejecutar una sentencia más que en el formato sin try-finally.
El try-catch-finally si que penaliza algo más porque se ejecuta el ‘catch’ y el código si lo hubiera. (El código dentro de un catch es muy similar al código que se ejecuta dentro de un ‘with’ ya que añade un objeto de activación más al contexto.
Aqui te paso un jsperf que he hecho al respecto:
En los ejemplos se ejecuta el mismo código, y en la version del try-catch-finally dejamos vacio el bloque catch para que el impacto sea el menor posible.
http://jsperf.com/check-nullify-pattern
Saludos y gran trabajo.
Gracias Tomás por la aportación. Las métricas son interesantes: por lo que vemos, el try/finally es incluso más lento que el try/catch/finally (aunque en este caso el catch esté vacío). En términos porcentuales, vemos más o menos lo siguiente (bajo Chrome) :
– Aplicar nullify es un 25% más lento que la función normal.
– El try/catch/finally es casi un 40% más lento que la función normal.
– El try/finally penaliza aproximadamente un 50% con respecto a la función normal.
Es ahí dónde tenemos que comenzar a sopesar si las ganancias en términos de memoria compensan el sacrificio de rendimiento en cuanto a velocidad de ejecución…
Por otra aparte, agradecerte también a ti la estupenda charla que diste en el MadridJS parte de la cual ha inspirado este artículo.
Saludos!
Hola!
Yo no soy experto en Javascript, asi que me puedo estar colando en alguna cosa. Pero asumiendo que la recoleccion de basura funciona de manera muy parecida a otras VM de tipado dinamico voy a soltar mi opinion.
Me parece que la descripcion del proceso de recoleccion es bastante imprecisa:
«el famoso recolector de basura cuya finalidad es eliminar de la memoria aquellas variables (y su valor) que ya no necesitamos.»
El recolector no elimina ninguna variable ni su valor: busca los objetos que no estan referenciados por ninguna variable. Esto es importante porque las variables (el espacio de memoria alojando una referencia o un valor) son tambien eliminadas, pero en un proceso muy diferente ligado a la pila de ejecucion.
La manera de funcionar de la mayoria de recolectores es recorrer el monticulo de objetos en memoria buscando los que no estan asignados a una variable para proceder a liberar el espacio. Este proceso implica que asignar null a una variable no tiene ningun efecto en la recoleccion, porque el recolector tiene que pasar por el objeto igualmente y «preguntar» por las referencias.
En la practica los recolectores son muchisimo mas avanzados que esto, y aplican reglas heuristicas para optimizar el proceso. La logica de marcar objetos para la recoleccion no es tanto «recoger» o «no recoger», sino algo mas difusa. Y cada VM tiene la suya diferente, como indicas en el articulo.
Es probable que alguna de esas reglas provoquen un efecto al asignar null a una variable local, pero eso puede variar, no solo entre lenguajes, sino entre VMs del propio lenguaje. Por lo que en mi opinion, asignar null a una variable local es inutil al no poder anticipar que resultados obtendremos al hacerlo.
Hola Miguel;
el recolector de basura en Javascript funciona de un modo algo diferente al que expones y es por ello que el patrón anterior cobra sentido.
Me queda pendiente un artículo completo sobre el tema, pero puedes investigar un poco sobre el tema en el siguiente enlace:
How Do The Script Garbage Collectors Work?
Un saludo!
No le veo mucho sentido el uso que le has dado a los try, los veo innecesarios cuando se puede simplemente almacenar el objeto temporalmente, operar y después devolver el objeto.
http://jsperf.com/finally-vs-container
Por cierto, se te han colado puntos y comas en el return del segundo y tercer ejemplo.
Tienes razón con los puntos y comas; ya los he añadido; gracias!
En cuanto a devolver un objeto en lugar de hacer un ‘try/finally’, sería un error: si con este patrón estamos precisamente evitando el mantener en memoria objetos innecesarios, el solo hecho de declarar uno nuevo que contenga los datos a devolver supone saltarnos el objetivo de la estructura. En tu ejemplo, la variable ‘result’ quedaría sin ‘nullificar’, por lo que no habríamos apenas conseguido nada: ésta permanecería en memoria almacenando el valor de los elementos DOM seleccionados…
Con la opción del ‘try/catch’ no dejamos variables por el camino sino que tratamos de, efectivamente, vaciarlas todas.
Saludos!
Pero es que retornar directamente un objeto y crear un objeto y retornarlo es exactamente lo mismo, ya que cuando lo retornas no se clona el objeto, sigue siendo el mismo objeto, pues nuevas declaraciones de variables apuntado a objetos son simples referencias. Dicho esto, no tiene sentido querer establecer a null el objeto que queremos retornar, ya que no estaremos liberando memoria.
El único caso particular en el que se quedaría el objeto dentro de la función en «el aire» sería que quisiéramos deshacernos del objeto retornado, y haciendo éste null no lo conseguiríamos, tendríamos que recorrer el objeto y eliminar una a una sus propiedades, de éste modo el objeto retornado y el que contiene la función serán objetos vacíos ({}).
Hola Carlos,
el artículo de MSDN describe lo mismo que yo, el mismo algoritmo en el que se basan casi todos los recolectores de basura en lenguajes orientados a objectos. Como dice ahí, el propio artículo tiene 9 años y está algo desfasado, pero los recolectores siguen teniendo mark ‘n sweep como base.
Yeikos tiene razón, tiendes a confundir la recolección de basura con el ciclo de vida de una variable. Tus variables locales «myElement» y «newElement» se eliminan en cuanto la función acaba su ejecución. El espacio que ocupan se queda libre de forma inmediata (un espacio despreciable, 4 u 8 bytes por variable).
Otra cosa son los objetos a los que apuntaban esa variable, los nodos del DOM. Éstos permanecerán en memoria hasta que el recolector de basura actue. Pegando del artículo:
«perhaps a GC will not run when you want one to. If you say «blah = null» then the memory owned by blah will not be released until the GC releases it.»
Es decir, que da lo mismo que hagas null a la variable o no, porque el objecto se eliminará de la misma forma en ambos casos: en la siguiente pasada del recolector, y después de preguntar al objeto si éste está referenciado por alguna variable.
Ya como curiosidad. Nunca se me había ocurrido algo como «try { return x } finally { x = null }», un bloque finally después del return. He hecho un dump ejecutando ese código en V8 para ver que pasa y resulta que internamente V8 genera una variable temporal en la que almacena el valor de x y que es devuelta al final del bloque.
es decir, en V8
«try { return x } finally { x = null }»
es exactamente igual que:
«var temp; try { temp= x } finally { x = null }; return temp;»
Quedando «temp» como una variable sin «nullificar». Aunque da lo mismo porque la variable se eliminará justo después del return.
Un saludo
Bueno lei varios comentarios y creo que puedo dar una opinion sobr esto, si haber leido bien como es que trabaja el garbage collector de cada VM, yo creo que el hecho de nullear las variables al finalizar la funcion no tiene mucho sentido, se pierde rendimiento para liberar memoria (milisegundos?) antes.
Ahora, si la veo genial para lo que es programacion asincronica y sin tener el concepto sabido lo vengo usando, y paso a explicar: Yo trabajo con Node.js, que es javascript del lado del servidor, internamente corriendo sobre la VM V8.
En node se programa de forma asincronica el 90% del tiempo, asi que si tenemos algo asi:
Si bien la funcion es corta, esta llamando a un metodo asincronico, y hasta que el callback de respuesta no termine, se tiene que mantener vivo el scope con todas las variables y referencias, asi que en este caso la referenia al objeto devuelto por Users.getUser() estaria gastando memoria hasta el final de la ejecucion de la query sin ser necesitado.
Dejaria al obejeto sin referencias por lo que al garbage collector no tendria que esperar que muera el scope completo.
Esto client-side, puede pasar con metodos asincronicos basados en eventos, como hace mucho jQuery o HTTPRequest (Ajax):
De esa forma (si ya se ele ejemplo es malo) la referencia hacia el nuevo objeto jQuery-node que encapsula al elemento #inputName seria recogido por el GC antes de que se termine la llamada Ajax.
Obviamente si lo usamos dentro de un callback, por ejemplo dentro del CB pasado en onsuccess, no lo podriamos nullear o seria inaxesible a la hora en que se ejecute ese CB.
Espero haber sido claro. Saludos.
Gracias por tu comentario Exos; es interesante el uso de este patrón en bajo el paradigma asincrónico.
Tengo pendiente el encontrar un hueco para elaborar la respuesta completa para el tema del recolector y su funcionamiento en JavaScript. En ella añadiré unas cuantas métricas que reflejen la carga en memoria de códigos con y sin ‘nullificar’.
Saludos!
¿Y esto no lo hace JS automáticamente al salir las variables de ámbito? Es decir, en el momento en el que se evalúa cualquier closure todas las variables ligadas a esta (en este caso las locales declaradas con var) se desechan automáticamente sin intervención del programador.
Pues si y no: las variables se desecha, y el espacio en memoria pasa a estar disponibles, pero NO en el momento en el que se sale del ámbito concreto sino cuando se requiere memoria porque se está llegando al final de la pila. Es por esto que algunos juegos en HTML5 se ‘congelan’ de vez en cuando: se está liberando memoria.
De ahí que sea interesante no dejar que el proceso de limpiado tenga que barrerlo todo, sino solo aquello que vamos indicando…
Hola Carlos.
Ni te imaginas lo que he disfrutado leyendo tus artículos. Me he leído 5 o 6 del tirón.
En este justamente, algo no me cuadra. Soy de la misma opinión que otros comentarios, de que no tiene sentido el colocar a 0 la variable para facilitarle el trabajo al GC.
Quizá es mi ignorancia sobre el GC de JavaScript, pero en las MV que conozco (incluso sistemas con punteros «shared» en C++ que usan la misma filosofía), un dato pierde una referencia cuando la variable que lo mantiene se borra.
En realidad se hace desde el «otro» punto de vista; una variable que se borra o se le cambia el dato que guarda, quita una referencia al dato «que sale» y añade una referencia al dato «que entra».
Sólo se me ocurre que JavaScript no borre las variables cuando salen de su ámbito de existencia y las «aparte». Y el GC de JavaScript primero borre esas variables y luego borre los datos cuyas referencias sean 0.
Gracias por todo y sigue así!
Muy buen articulo