El compilador closure (no confundir con el concepto de closure) es una de las múltiples herramientas disponibles para mantener nuestro código Javascript un poco más saludable. Es la opción que mejor conozco, y por eso voy a comentar cómo funciona.
Closure es un compilador capaz de convertir lenguaje Javascript extendido con anotaciones (en los comentarios), a otro Javascript optimizado para su ejecución y habitualmente más pequeño. Además, realiza un análisis estático de nuestro código para encontrar errores comunes y verificar las anotaciones. En mi opinión, este análisis estático es la funcionalidad más interesante de este compilador, ya que existen múltiples minificadores de código que resuelven la otra parte. Un ejemplo de estas anotaciones:
/** * @param {number} x Un argumento cualquiera. * @constructor * @extends {MiOtraClase} */ function MiClase(x) { this.x = x; } /** * @override */ MiClase.prototype.mostrar = function() { window.console.log(this.x); }
Tipos
Este compilador permite, entre otras muchas cosas, declarar tipos estáticos y verifica que el código satisface los contratos introducidos por dichos tipos. Una gran ventaja de este compilador es que los tipos se introducen mediante comentarios, con una sintaxis muy parecida a Javadoc o Doxygen, como se puede apreciar en el ejemplo anterior. Esto tiene dos consecuencias muy interesantes:
- El tipado no es intrusivo, el código sigue siendo Javascript, y se puede evaluar en cualquier intérprete de este lenguaje (por ejemplo, cualquier navegador).
- Fomenta la documentación del código. Ya que tengo que añadir @param {tipo} nombre para que el compilador haga el trabajo sucio y valide los tipos, no cuesta nada escribir un poco más y explicar para qué sirve el parámetro.
Este es un ejemplo del tipo de problemas que resuelve este compilador:
function suma(a, b) { return a + b; } var resultado = suma("10", 2); window.console.log(resultado);
Este código imprime por pantalla «102», en lugar de «12». Dado que se utilizan constantes en el ejemplo, es muy fácil ver el error y solucionarlo, pero si el primer argumento fuese en realidad el valor tomado de un campo de texto HTML, entonces ya no sería tan evidente, aunque el problema sería el mismo. Este es el mismo ejemplo tipado por closure:
/** * @param {number} a Primer operando. * @param {number} b Segundo operando. * @return {number} El resultado. */ function suma(a, b) { return a + b; } var resultado = suma("10", 2); window.console.log(resultado);
Si ahora se ejecuta el compilador, se obtiene lo siguiente:
$ java -jar compiler.jar --warning_level VERBOSE \ test.js > /dev/null /tmp/test.js:9: WARNING - actual parameter 1 of suma does not match formal parameter found : string required: number var resultado = suma("10", 2); ^
En el ejemplo anterior, se está redirigiendo la salida del compilador (el código minimizado y optimizado) a /dev/null. En entornos de producción esa salida puede ser muy interesante por las mejoras de velocidad de carga y ejecución que implica, pero la idea que quiero transmitir aquí es que incluso si no interesa ese código «minificado», el compilador aporta mucho valor al analizar el código y encontrar errores.
Estas anotaciones no se quedan simplemente en parámetros y variables, permiten declarar interfaces, proteger sobrecargas con @override igual se haría en Java, anotar herencia, declarar constructores, visibilidad, e incluso nombrar tipos complejos por comodidad. Aquí hay una referencia bastante actualizada de las posibilidades. En versiones recientes del compilador hay algunas funciones experimentales como tipos genéricos (similar a los generics de Java o los templates de C++), que dotan de más expresividad al sistema de tipado y todavía no están documentadas en dicha referencia, aunque algunas bibliotecas ya los usan.
Optimizaciones
Ya que se trata de un compilador, y no un simple parser capaz de reducir espacios en blanco y renombrar variables, es capaz de hacer algunas cosas típicas de compiladores, como inlining y precálculo de expresiones. Un ejemplo:
/** * @param {number} x * @param {number} y * @return {number} */ function suma(x, y) { return x + y; } window.console.log(suma(3, 2));
Es bastante evidente que este código va a mostrar en la consola del navegador el número 5, y sin embargo la mayoría de minificadores generarán un código compacto que aún declarará la función y la llamará. Closure es capaz de ir un poco más allá:
$ java -jar compiler.jar --warning_level VERBOSE \ --compilation_level ADVANCED_OPTIMIZATIONS \ test2.js > /dev/null window.console.log(5);
En este caso el compilador descubrió que podía precalcular el resultado de llamar a la función suma e inyectarlo en lugar de la llamada a la función, resultado en un código más compacto y rápido de ejecutar. En muchos casos es necesario tener información de tipos para hacer optimizaciones como esa. Todavía no alcanza, sin embargo, los niveles de GCC, capaz de precalcular incluso el resultado de funciones recursivas (por desgracia GCC todavía no soporta Javascript).
No intrusivo
Tanto si uno está interesado en las optimizaciones que closure puede realizar como si no, este compilador es una herramienta muy interesante porque no genera una dependencia tecnológica en un proyecto. Se puede eliminar y todo el código del proyecto seguirá funcionando igual. Además, puede convivir perfectamente con otras herramientas de análisis estático de código Javascript que encuentren otros errores y vulnerabilidades, o con otros minificadores. Es cierto que obliga a describir un poco mejor los contratos del código, indicando tipos, herencias, etc. de una forma homogénea, y esto, en mi opinión, mejora mucho la legibilidad del código, y es un trabajo que se debería hacer en cualquier caso si se pretende que el código sea mantenible.