Javascript es un lenguaje incomprendido. Es
caso a poco, esta situación va cambiando y el lenguaje se está regresando bastante popular últimamente. No voy a engañar a nadie, yo mismo echaba pestes de Javascript hace no mucho tiempo. Tras dedicarle el tiempo que se merece a comprenderlo, mi
punto de vista ha cambiado bastante. Lenguaje dinámico El concepto de lenguaje dinámico es un escaso abstracto, pero usualmente se refiere a retrasar a la hora de la ejecución muchas de las comprobaciones que los otros lenguajes realizan mientras la compilación. Por ejemplo, al escribir objeto.miMetodo(), en un lenguaje estático, mientras la compilación ya se comprueba si la variable
objeto tiene o no un método llamado miMetodo, porque felicidad variable tiene un tipo estático. En un lenguaje dinámico, esta comprobación se retrasa hasta la hora de ejecutar esa línea de código, porque es probable que el método se haya creado mientras la ejecución del programa, o que la variable objeto aloje distintos
tipos mientras la ejecución, y no todos ellos tengan ese método. Javascript tiene tipos, pero dado que se trata de un lenguaje dinámico, los tipos no determinan contratos,
como sucede en lenguajes con tipos estáticos, porque las propiedades de un tipo
pueden cambiar mientras la ejecución. Además, las
variables no están asociadas a un tipo estático para siempre (
como sucedería por
ejemplo en Java), sino que pueden alojar
objetos de distintos
tipos en distintos momentos. Esto ofrece una vez más mucha flexibilidad, aunque por desgracia, la ausencia de contratos en la manifiesto de
objetos y funciones, es muy propensa a errores. Ejemplo: function maximo(lista) { var
maximo = lista[0]; for (var i = 1; i lista.length; i++) { maximo = Math.max(maximo, lista[i]); return maximo; En otros lenguajes, habría que establecer un tipo estático para el argumento lista, que lo limitaría a un tipo particular o a cumplir con una interfaz específica (definiría un contrato). En Javascript no es así, y este código, aparentemente correcto, puede fallar de múltiples maneras debido a la falta de contratos de Javascript: Si en espacio de una lista, se llama con un diccionario (un objeto), no devolverá el máximo de sus valores, sino probablemente undefined (igual que para una lista vacía). Si además, el objeto tiene un atributo length, intentará iterar y alcanzar a
atributos ?0″, ?1″, etc. Si la lista contiene varios strings, Math.max devolverá NaNSi la lista contiene un solo string, devolverá ese string. Si la lista contiene
datos de distintos
tipos, lo más posible es conseguir un NaNPor supuesto, estos dificultades no son exclusivos de Javascript, son
comunes a todos los lenguajes dinámicos. Lo mismo puede suceder en Python o PHP, por
ejemplo (aunque su comportamiento ante índices no válidos es algo distinto). La manera habitual de detectarlos es
utilizar analizadores estáticos de código,
como JSLint. También tienen lugar lenguajes alternativos,
como TypeScript, que se transforman en Javascript y soportan tipado estático. Orientado a objetos Esto imagino que no sorprende a nadie a estas alturas. Efectivamente Javascript es orientado a objetos, pero no tiene
clases ni
herencia de clases. Tiene prototipos, y
herencia de prototipos. Es un paradigma un escaso diferente, y puede resultar confuso porque también tiene lugar un constructor y un operador new, aunque no funcionan del mismo modo que en lenguajes con clases. Básicamente, en Javascript hacen menos cosas, porque los prototipos son más simples que las clases. Este es uno de los puntos por los que Javascript recibe más críticas, porque a menudo se emplean los prototipos sin comprenderlos de verdad, o
pensando que son clases, y
como siempre que se emplea una tecnología que no se comprende, cuesta mucho depurar errores. La mejor manera de comprender qué es un prototipo es recurrir al célebre patrón de diseño. Un prototipo es un ejemplo de instancia, NO una clase. Todos los
objetos de Javascript tienen un prototipo, inclusive los números. Este es un ejemplo de uso de prototipos en Javascript: function Rectangulo(ancho, alto) { this.ancho = ancho; this.alto = alto; Rectangulo.prototype.area = function() { return this.ancho * this.alto; ; Es interesante ver la
diferencia entre una instancia creada con el operador new y el prototipo. Usando la consola de
javascript de vuestro navegador, esto es lo que se puede ver al crear una instancia: > new Rectangulo(2,2) Rectangulo {ancho: 2, alto: 2, area: function alto: 2 ancho: 2 __proto__: Rectangulo area: function () { constructor: function Rectangulo(ancho, alto) { __proto__: Object Y esto es lo que se ve al presentar el prototipo: > Rectangulo.prototype Rectangulo {area: function area: function () { constructor: function Rectangulo(ancho, alto) { __proto__: Object La instancia creada con el operador n y Rectangulo.prototype son muy parecidos, ambos son objetos. Y
como tales, ambos tienen un prototipo del que heredan propiedades. En este caso, la instancia creada con el operador new, tiene
como prototipo a Rectangulo.prototyp y Rectangulo.prototype tiene
como prototipo a Object. Object tiene un prototipo nulo, porque es el prototipo más genérico que hay. Algo que puede resultar confuso aquí, es que Rectangulo.prototype no es el prototipo de Rectangulo Rectangulo es una función, y su prototipo ( Rectangulo.__proto__ ) es el prototipo genérico de las funciones. En realidad, Rectangulo.prototype es el prototipo con el que se van a crear las instancias de Rectangulo al escribir new Rectangulo() Llegados a este punto, alguien se podría preguntar cómo se
diferencia una función usual de un constructor. La respuesta es sencilla, no se diferencian (salvo en el caso de
funciones nativas). Son la misma cosa. Se puede aplicar el operador new
sobre cualquier función, y se puede llamar a cualquier constructor
como si fue una función. La única diferencia es la intención con la que se escriben. Por tanto, es una buena práctica documentar la intención en vuestro código. Siguiendo con la analogía del patrón de diseño, Rectangulo.prototype es en verdad un ejemplo de rectángulo. Cuando se crea una instancia con el operador new, se crea un nuevo objeto, cuyo ejempl es Rectangulo.prototype. La idea es que al alcanzar a cualquier propiedad de un objeto (sea un atributo o una función), primero se mira si el propio objeto la tiene, y sino, se mira en su prototipo, y si tampoco la tiene, en el prototipo del prototipo, y así sucesivamente. En este sentido, es muy parecida a la
herencia clásica. De hecho, se podría agregar ancho y alto al prototipo, y la instancia creada con el operador new simplemente tendría otra versión de estas propiedades, con preferencia
sobre las del prototipo. El operador new hace algo más que crear un objeto vacío y establecer su prototipo, asimismo llama al constructor, que usualmente introduce atributos, y a veces nuevas funciones. Implementar
herencia de clases sobre este
modelo no es en verdad nada complicado, aquí se explica muy bien (en inglés). La clave es el método Object.create, que crea un nuevo objeto con un algun prototipo (
como new, pero sin llamar a constructores). Hay multitud de bibliotecas y precompiladores que proporcionan dispositivos para tener
herencia de clases cómodamente. Un ejemplo es el lenguaje CoffeeScript, que soporta herencia clásica y, al idéntico que TypeScript, también se transforma a Javascript. El paradigma de prototipos es, nuevamente, muy flexible. Se faculta cambiar los prototipos en tiempo de ejecución, inclusive los de clases proporcionadas por el lenguaje,
como Array y Object. Un constructor puede inclusive cambiar el prototipo mientras la construcción. De nuevo, es un arma de doble filo. Closures Pido permiso para
usar el término original, porque ninguna de las traducciones a español me convence. En lenguajes sin closures, desde el cuerpo de una función sólo son visibles las
variables declaradas en ese cuerpo, y las
generales (y si es un método, los atributos del objeto). En lenguajes con closures, desde el cuerpo de la función también son visibles las
variables declaradas en el entorno al que pertenece la función. Un ejemplo: function main() { var persona = { nombre: 'Juan' ; function reaccionarAEvento() { window.console.log(persona.nombre); capturarEvento('click', reaccionarAEvento); En este ejemplo hay dos entornos. El primer entorno es el más exterior, el de la función main. El segundo entorno es el de la funcion reaccionarAEvento. Además, se llama a otra función que faculta capturar eventos dado un nombre de evento y una función callback. La novedad, dado que Javascript soporta closures, es que el cuerpo de reaccionarAEvento está accediendo a la variable persona, que ha sido declarada en el entorno de mainEste concepto tan potente faculta programar mucho más rápido, ya que no es indispensable crear objetos que transporten estado y tengan un método para crear un callback. En Javascript el estado presente en el entorno se guarda automáticamente. Además, Javascript tiene funciones anónimas, así que el ejemplo previo se puede escribir de manera inclusive más compacta: function main() { var persona = { nombre: 'Juan' ; capturarEvento('click', function() { window.console.log(persona.nombre); ); Nuevamente, toda esta flexibilidad tiene un precio. Una función siempre conserva referencias a todos los objetos visibles en el punto donde se ha declarado. Todos implica no sólo aquellos que interesa que conserve, ni siquiera aquellos que se usan de manera explícita. Todos implica todos. Un ejemplo: function CreaClosure(x) { var y = { z: 5 ; return function(expresion) { return eval(expresion); var f = CreaClosure(2); window.console.log(f('x + y.z')); En este caso, la función CreaClosure está formando y devolviendo otra función. Esta función anónima que devuelve es un closure, conserva referencias a todos los objetos del entorno de CreaClosure, como el parámetro y el objeto . Uno podría pensar que, como aparentemente no los usa, el lenguaje optimizará y no guardará las referencias, pero no es así. La función que se devuelve, conserva referencias a ambos objetos inclusive aunque no se usen. Para demostrarlo, la función devuelta por CreaClosure recibe como argumento una expresión en texto, y la evalúa. En este ejemplo, se aplica una expresión que va a colocar a prueba el closure, accediendo al entorno de CreaClosure. Y al ejecutar este código en un navegador, se puede verificar que efectivamente funciona e imprime ¿Por qué es significativo todo esto? Porque una de las formas más habituales de crear memory leaks en Javascript, es a través de los closures. Si la variable , en espacio de un simple objeto chico sin importancia, fue un objeto más complejo, cuya referencia interesa que desaparezca al terminar CreaClosure, en verdad se estaría perpetuando esa referencia mientras mucho más tiempo (mientras la vida de la función anónima), inclusive aunque la función anónima no la utilice thisUn yerro usual al usar closures es asumir que this también estará disponible en las funciones declaradas dentro de un método. Un ejemplo: Interfaz.prototype.registrarEventos() { this.boton.onClick = function() { window.console.log('Has pulsado ' + this.boton.name); El yerro en este código está en this.boton.name, y la razón es que
this no se propaga a los closures, ya que son funciones y en ocasiones tienen su propio this. A veces inclusive el this que está disponible en un callback es algo completamente diferente, como sucede habitualmente en jQuery. Es tradicional ver código que almacena this en otra variable (por ejemplo, self, en honor a Python) para que se inyecte en el closure y poder utilizarlo a continuación, ej: Interfaz.prototype.registrarEventos() { var self = this; this.boton.onClick = function() { window.console.log('Has pulsado ' + self.boton.name);