Comprendiendo Javascript

Como dijo Douglas Crockford hace más de 10 años, Javascript es un lenguaje incomprendido. Poco a poco, esta situación va cambiando y el lenguaje se está volviendo 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 poco abstracto, pero normalmente se refiere a retrasar a la hora de la ejecución muchas de las comprobaciones que los otros lenguajes realizan durante la compilación. Por ejemplo, al escribir objeto.miMetodo(), en un lenguaje estático, durante la compilación ya se comprueba si la variable objeto tiene o no un método llamado miMetodo, porque dicha 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 posible que el método se haya creado durante la ejecución del programa, o que la variable objeto aloje diferentes tipos durante 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 definen contratos, como sucede en lenguajes con tipos estáticos, porque las propiedades de un tipo pueden cambiar durante 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 diferentes momentos. Esto proporciona nuevamente mucha flexibilidad, aunque por desgracia, la ausencia de contratos en la declaración 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 específico 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 formas debido a la falta de contratos de Javascript:

  1. Si en lugar 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).
  2. Si además, el objeto tiene un atributo length, intentará iterar y acceder a atributos “0”, “1”, etc.
  3. Si la lista contiene varios strings, Math.max devolverá NaN.
  4. Si la lista contiene un solo string, devolverá ese string.
  5. Si la lista contiene datos de diferentes tipos, lo más probable es obtener un NaN.

Por supuesto, estos problemas 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 forma habitual de detectarlos es utilizar analizadores estáticos de código, como JSLint. También existen 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 poco diferente, y puede resultar confuso porque también existe 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 utilizan los prototipos sin comprenderlos de verdad, o pensando que son clases, y como siempre que se utiliza una tecnología que no se comprende, cuesta mucho depurar errores.

La mejor forma de entender qué es un prototipo es recurrir al famoso patrón de diseño. Un prototipo es un ejemplo de instancia, NO una clase. Todos los objetos de Javascript tienen un prototipo, incluso 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 nuestro 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 mostrar el prototipo:

> Rectangulo.prototype
  Rectangulo {area: function}
    area: function () {
    constructor: function Rectangulo(ancho, alto) {
    __proto__: Object

La instancia creada con el operador new 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.prototype 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 normal 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 fuese 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 nuestro código.

Siguiendo con la analogía del patrón de diseño, Rectangulo.prototype es en realidad un ejemplo de rectángulo. Cuando se crea una instancia con el operador new, se crea un nuevo objeto, cuyo ejemplo es Rectangulo.prototype. La idea es que al acceder 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 similar a la herencia clásica.

De hecho, se podría añadir 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, además llama al constructor, que normalmente introduce atributos, y a veces nuevas funciones.

Implementar herencia de clases sobre este modelo no es en realidad 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 determinado prototipo (como new, pero sin llamar a constructores). Hay multitud de bibliotecas y precompiladores que ofrecen herramientas para tener herencia de clases cómodamente. Un ejemplo es el lenguaje CoffeeScript, que soporta herencia clásica y, al igual que TypeScript, también se transforma a Javascript.

El paradigma de prototipos es, nuevamente, muy flexible. Se permite cambiar los prototipos en tiempo de ejecución, incluso los de clases proporcionadas por el lenguaje, como Array y Object. Un constructor puede incluso cambiar el prototipo durante 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 globales (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 ámbito 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 ámbitos. El primer ámbito es el más exterior, el de la función main. El segundo ámbito es el de la funcion reaccionarAEvento. Además, se llama a otra función que permite 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 ámbito de main.

Este concepto tan potente permite programar mucho más rápido, ya que no es necesario crear objetos que transporten estado y tengan un método para crear un callback. En Javascript el estado presente en el ámbito se guarda automáticamente. Además, Javascript tiene funciones anónimas, así que el ejemplo anterior se puede escribir de forma incluso 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 significa no sólo aquellos que interesa que conserve, ni siquiera aquellos que se usan de forma explícita. Todos significa 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á creando y devolviendo otra función. Esta función anónima que devuelve es un closure, conserva referencias a todos los objetos del ámbito de CreaClosure, como el parámetro x y el objeto y. 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 incluso 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 poner a prueba el closure, accediendo al ámbito de CreaClosure. Y al ejecutar este código en un navegador, se puede comprobar que efectivamente funciona e imprime 7.

¿Por qué es importante todo esto? Porque una de las maneras más habituales de crear memory leaks en Javascript, es a través de los closures. Si la variable y, en lugar de un simple objeto pequeño sin importancia, fuese un objeto más complejo, cuya referencia interesa que desaparezca al terminar CreaClosure, en realidad se estaría perpetuando esa referencia durante mucho más tiempo (durante la vida de la función anónima), incluso aunque la función anónima no la utilice.

this

Un error frecuente 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 error 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 incluso el this que está disponible en un callback es algo completamente diferente, como sucede habitualmente en jQuery. Es típico 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 usarlo a continuación, ej:

Interfaz.prototype.registrarEventos() {
  var self = this;
  this.boton.onClick = function() {
    window.console.log('Has pulsado ' + self.boton.name);
  }
}

Existen librerías que implementan funciones para inyectar this en un closure sin necesidad de usar otro nombre. Un ejemplo es la librería closure y su goog.bind.

Y esto es todo por ahora. Gracias por acompañarme hasta el final, espero que te haya resultado útil. No dudes en comentar si tienes alguna duda.

Gracias a Ricardo Rodríguez por revisar este artículo!

Anuncios

Acerca de Rubén L.

Software Engineer
Esta entrada fue publicada en Español, ingeniería y etiquetada , , , , . Guarda el enlace permanente.

5 respuestas a Comprendiendo Javascript

  1. Diego dijo:

    Excelente artículo! un buen repaso a muchas de las características que hacen a Javascript tan querido (y odiado), desde conceptos generales a cosas muy específicas. Para cuando un libro? Qué digo libro! para cuando una línea editorial de libros de programación?? Los O’Lopez. Serían excelentes..

  2. Pingback: El compilador “closure” | Rubén López

  3. *O* muy buen post… llegue aqui justamente por lo de los closures ya que estoy aprendiendo javascript y a usar ajax, en el pdf que estoy leyendo “Introduccion a AJAX” hacen un pequeño script para utilizar ajax y bueno me propuse como desafio hacer uno propio :$ !

    si le hecharas un ojo a mi codigo es super corto y facil de entender http://paste2.org/9EIJnamw
    , no se como explicar mi duda ya que ni yo la entiendo muy, pero hare mi mejor intento D: !

    al hacer una peticion > lp.ajax({})
    se hace referencia al objeto xhr que esta dentro del closure…
    entonces al hacer otra peticion > lp.ajax({})
    ¿se crea otra instancia del objeto xhr ?
    ya que lei por ahi que cada llamada al closure es independiente de otra, osea aunque utilicen la misma variable no interfieren entre si, son totalmente independientes :/
    bueno entendi mas o menos el concepto pero en la practica no se identificar muy bien las cosas ajajajajaja por eso si tienes 5 minutos algun dia para hecharle una mirada al script te lo agradeceria infinitamente 😀

    pd: aqui lei sobre los closures http://www.variablenotfound.com/2012/10/closures-en-javascript-entiendelos-de.html

    • Rubén L. dijo:

      Hola Luis,

      En ese ejemplo que envías, sólo se va a crear una instancia del objeto xhr. Se declara en la función anónima que aparece arriba de todo, y esa función anónima sólo se llama una vez abajo del todo.
      En un navegador sólo hay un thread, así que no puede haber dos hilos manipulando el objeto xhr al mismo tiempo. Existe el concepto de workers, que sí corren en threads concurrentes, pero no pueden usar XHR.
      De todas formas, tienes un problema si inicias dos peticiones asíncronas en paralelo, porque cada vez que haces una petición, estás instalando un callback para el evento onreadystatechange que sobreescribe al anterior. El problema aquí, es que cuando cualquiera de las peticiones asíncronas termine, se intentará llamar al callback del objeto xhr, y el callback que vas a tener ahí es el último que has asignado.
      En resumen, creo que tienes dos opciones:
      1. En lugar de guardar el objeto xhr en el closure, define una función getXhr() que devuelva un nuevo objeto cada vez que la llamas, y usa esa función desde tu método ajax().
      2. Si esperas tener muchas peticiones, y para evitar sobrecargar el recolector de basura de javascript, puedes crear un pool de objetos xhr. Cuando empiezas una petición sacas un objeto del pool, y cuando termina, lo devuelves.

      Esto responde tu pregunta?

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s