Archivo de 'javasctipt'

Introducción a la programación en Facebook III

Tercera parte de esta guía de introducción a la programación de aplicaciones en Facebook. Si seguiste la primera y la segunda parte, a estas alturas ya le habrás perdido el medio a muchos de los conceptos clave (tipos de aplicaciones, SDKs, Social Plugins, Open Graph, XFBML, Insights). Pero hay otros conceptos igualmente importantes que todavía no conocemos. Hoy vamos a tratar la autenticación OAuth, el SDK de JavaScript a fondo, y veremos cómo crear diálogos.


Read more

Introducción a la programación Facebook II

Ok, el anterior artículo no estuvo mal; vimos una introducción conceptual a las piezas clave para la construcción de aplicaciones en Facebook. ¡Pero no llegamos a hacer nada! En este post vamos a construir algo sencillo, jugando con el SDK en JavaScript. Vamos a probar a mostrar algunos de los social plugins, usaremos Open Graph, y presentaremos dos nuevas tecnologías: XFBML y Facebook Insights. ¡Al lío!


Read more

Introducción a la programación en Facebook

¿De modo que te han pedido que hagas una aplicación para Facebook y no sabes por dónde empezar? ¿Has llegado a la documentación oficial y te pierdes entre tanto Open Graph, SDK, plugins, dialogs y Graph API? Don’t panic! En este artículo vamos a ver paso a paso cómo empezar a programar en el nuevo sistema de Facebook. ¡Al lío!


Read more

Bibliotecas JavaScript: Motores de física

Hoy hemos visto en el Google I/O la noticia de que Angry Birds estará disponible para el navegador Google Chrome. ¿Cómo es esto posible? Gracias al objeto canvas :D Ahora que HTML5 ha llegado para quedarse, y que incluso Internet Explorer ofrece soporte para el elemento canvas, poco a poco se irán popularizando las animaciones y juegos en el navegador, sin necesidad de Flash. Uno de los elementos clave son los motores de física; consiste en una serie de bibliotecas que permiten modelar sistemas donde exista gravedad, y se puedan definir objetos rígidos o dinámicos, con coeficientes de fricción, elasticidad, etc. Suena bien, ¿verdad? :)

Uno de los motores más populares es box2d. De hecho, es el que utiliza el propio Angry Birds. Y por supuesto existen ports de esta biblioteca para muchísimos lenguajes de programación, incluido, claro está, el pequeño gran JavaScript. En concreto la versión JavaScript está extraída automáticamente de la versión de ActionScript. Si quieres ver algunos ejemplos en vivo, entra a la web de box2d-js. Lo que ahora vamos a hacer, lo puedes ver aquí abajo (prueba a hacer clic sobre el lienzo).

Dejemos de hablar, y vamos a mancharnos las manos :)

Aviso sobre este artículo

El código en el que he basado este artículo está extraído de savagelook.com. Te recomiendo que entres y lo revises, ya que su ejemplo es más completo que la versión que yo aquí expongo.

Del mismo modo, es muy recomendable el manual de uso de box2d que han hecho en Box2DFlash.

Paso 1 – Preparando el entorno

Lo primero que tenemos que hacer es crear el código HTML. box2djs depende de dos bibliotecas:

  1. Prototype, como framework javascript.
  2. excanvas, para añadir soporte canvas a Internet Explorer.

Además, crearemos ya el objeto canvas, al que le estableceremos el ancho y alto, y sus propiedades CSS. Nos quedará el siguiente código:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!--[if IE]><script type="text/javascript" src="lib/excanvas.js"></script><![endif]-->
<script src="lib/prototype-1.6.0.2.js"></script>
<script src="lib/box2djs.min.js"></script>
</head>
<body>
<canvas id="canvas-world" style="position:absolute; top:0; left:0;background-color:#F5F5F5;width:800px; height:500px;"></canvas>
</body>
</html>

Ver demo. Como vemos, no ocurre gran cosa. Tan sólo vemos una gran caja gris

Paso 2 – Crear el mundo

En este paso vamos a crear el mundo en el que todo sucederá. Vamos a ver el código JavaScript que hace lo define:

            var world;
            var ctx;
            var canvasWidth;
            var canvasHeight;

            // Función que crea el mundo.
            function createWorld() {
                var worldAABB = new b2AABB();
                worldAABB.minVertex.Set(-1000, -1000);
                worldAABB.maxVertex.Set(1000, 1000);
                var gravity = new b2Vec2(0, 300);
                var doSleep = true;
                world = new b2World(worldAABB, gravity, doSleep);

                return world;
            }

            // Punto de entrada principal. Cuando se cargue la ventana:
            Event.observe(window, 'load', function() {
                world = createWorld();
       		ctx = $('canvas-world').getContext('2d');

       		var canvasElm = $('canvas-world');
       		canvasWidth = parseInt(canvasElm.width);
       		canvasHeight = parseInt(canvasElm.height);
       		var canvasTop = parseInt(canvasElm.style.top);
       		var canvasLeft = parseInt(canvasElm.style.left);
            });

En primer lugar definimos cuatro variables globales que utilizaremos en el resto de la aplicación. Después, definimos la función createWorld(), y finalmente vinculamos el evento “load” de la ventana, al código de la función anónima. Este código por ahora hace una llamada a createWorld(), extrae el lienzo 2d del canvas, y calcula su ancho/alto y sus coordenadas. Pero vamos a ver qué hace createWorld():

La primera línea crea un nuevo objeto de tipo b2AABB. Estos objetos definen un espacio en 2 dimensiones. En las dos líneas siguientes establecemos los extremos donde este espacio termina. En concreto nuestro espacio, worldAABB, va desde las coordenadas (-1000,-1000) hasta las coordenadas (1000,1000).

Después creamos un vector gravitacional. El primer valor, 0, corresponde a la gravedad horizontal. El segundo, 300, la gravedad vertical. Es decir, podemos crear una gravedad lateral :D

Otra opción que definimos es la posibilidad de que los objetos se “duerman” cuando entren en reposo. Esto lo hacemos asignando true a la variabel doSleep.

Finalmente creamos el objeto mundo, de la clase b2World, pasándole como parámetros al constructor las variables que acabábamos de crear.

Ver demo. En este caso, de nuevo no ocurre nada. Tan sólo hemos definido el modelo virtual, sin atarlo al canvas.

Paso 3 – Crear un suelo

En nuestro ejemplo va a haber un suelo. En resumen, tan sólo es una caja con posición fija ocupando toda la parte inferior visible. Este es el código que lo define:

            // Añade un suelo al mundo
            function createGround(world) {
                var groundSd = new b2BoxDef();
                groundSd.extents.Set(400, 30);
                groundSd.restitution = 0.0;
                var groundBd = new b2BodyDef();
                groundBd.AddShape(groundSd);
                groundBd.position.Set(400, 470);
                return world.CreateBody(groundBd);
            }

Además, añadimos una llamada a createGround(world) antes del return de la función createWorld(). Vamos a ver en qué consiste la creación del suelo:

Primero creamos un objeto b2BoxDef, que no es más que un objeto que define una forma rectangular. A continuación establecemos su tamaño (400 x 30), y su elasticidad (0.0). El factor de elasticidad 0.0 es el mínimo. Cuanto más alto, más elástico será el material de la forma que estamos definiendo.

A continuación creamos un cuerpo físico, al que le añadimos la forma que acabamos de crear. Por último establecemos las coordenadas en las que lo colocamos (400, 470), y lo añadimos al mundo.

Ver demo. Todavía no ocurre nada. Tan sólo hemos añadido la definición y las características del suelo, pero nuestro mundo todavía no se ha arrancado, ni se ha vinculado al canvas.

Paso 4 – Dibujando en el lienzo

Bien, ya vamos avanzando :) En este paso vamos a implementar el código encargado de dibujar en el canvas nuestro mundo virtual. Veamos cómo es:

            function drawWorld(world, context) {
                for (var b = world.m_bodyList; b; b = b.m_next) {
                    for (var s = b.GetShapeList(); s != null; s = s.GetNext()) {
                        drawShape(s, context);
                    }
                }
            }

            function drawShape(shape, context) {
                context.strokeStyle = '#ffffff';
                context.fillStyle = "black";
                context.beginPath();
                switch (shape.m_type) {
                    case b2Shape.e_polyShape:
                        {
                            var poly = shape;
    				        var tV = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[0]));
                            context.moveTo(tV.x, tV.y);

                            for (var i = 0; i < poly.m_vertexCount; i++) {
                                var v = b2Math.AddVV(poly.m_position, b2Math.b2MulMV(poly.m_R, poly.m_vertices[i]));
                                context.lineTo(v.x, v.y);
                            }
                            context.lineTo(tV.x, tV.y);
                        }
                        break;
                }

                context.fill();
                context.stroke();
            }

Si nunca has trabajado con canvas, este es un buen momento para que revises algún artículo. Por ejemplo, este de Mozilla, o este de Thinkvitamin.

Por un lado tenemos la función drawWorld, que se encarga de recorrer cada uno de los objetos del mundo, y cada una de las formas que contienen los objetos, para finalmente llamar a la función drawShape, que dibujará en el lienzo canvas la forma.

Por otro lado tenemos drawShape, que es la función que dibuja cada forma. En primer lugar establecemos los colores de línea (stroke) y de relleno (fill). Posteriormente indicamos que vamos a comenzar a dibujar en el lienzo. A continuación dibujamos la forma que corresponda; por ahora tan sólo dibujamos polígonos.

Para cada polígono, calculamos las coordenadas de su primer vértice. A continuación, dibujamos líneas entre cada uno de los siguientes vértices que encontremos, hasta finalmente, cerrar el polígono.

Y por último indicamos al contexto del lienzo que renderice el relleno y las líneas.

Ver demo. ¡Cómo es posible! ¡Todavía no ocurre nada! Esto es porque falta la función que da vida y que arranca al sistema. ¡Vamos al siguiente paso!

Paso 5 – Arrancando el sistema

Ya ha llegado el momento; vamos a unir nuestro sistema, con nuestro canvas, y a hacer que funcione en el tiempo. Para ello vamos a añadir una nueva función:

            function step(cnt) {
                var timeStep = 1.0/60;
                var iteration = 1;
                world.Step(timeStep, iteration);
                ctx.clearRect(0, 0, canvasWidth, canvasHeight);
                drawWorld(world, ctx);
                setTimeout('step(' + (cnt || 0) + ')', 10);
            }

Y vamos a hacer que la función anónima que se ejecuta al cargar la página, invoque a esta función, añadiendo al final:

step();

¿Qué hace step()? Cada vez que se invoca, realiza una nueva iteración en el sistema. En primer lugar establece los frames per second del sistema físico. Por convenio suele utilizarse a 60Hz, es decir, 1.0/60. En cada uno de estos instantes, se realizará una simulación con las equaciones físicas.

A continuación definimos el número de iteraciones por cada instante. Cuanto más bajo sea el valor, más precisión, así como mayor consumo de recursos.

En la tercera línea, indicamos al mundo que avance un nuevo paso. Es en este momento cuando se realizan todos los cálculos para ver en qué estado se encuentra cada objeto del sistema.

Después, limpiamos el lienzo, ya que en la siguiente línea hacemos una llamada a drawWorld para dibujar todo el sistema.

Finalmente establecemos un timeout para volver a ejecutar un paso (step()) en los siguientes 10 milisegundos.

Ver demo. ¡Viva! Al fin se ve algo :D No es gran cosa, pero ya tenemos dibujado el suelo en nuestro lienzo.

Paso 6 – Añadiendo más objetos al mundo

El paso anterior ha estado bien, pero vamos a hacer que nuestro mundo sea más rico en detalles. En concreto vamos a añadir unos polígonos que escriban el mensaje “Hola Mundo!” :) Esta es la función que lo crea:

            function createBox(world, x, y, width, height, fixed) {
                if (typeof(fixed) == 'undefined') fixed = true;
                var boxSd = new b2BoxDef();
                if (!fixed) boxSd.density = 1.0;
                boxSd.restitution = 0.0;
                boxSd.friction = 1.0;
                boxSd.extents.Set(width, height);
                var boxBd = new b2BodyDef();
                boxBd.AddShape(boxSd);
                boxBd.position.Set(x,y);
                return world.CreateBody(boxBd);
            }

            function createHelloWorld() {
                // H
                createBox(world, 50, 420, 10, 20, false);
                createBox(world, 90, 420, 10, 20, false);
                createBox(world, 70, 395, 30, 5, false);
                createBox(world, 50, 370, 10, 20, false);
                createBox(world, 90, 370, 10, 20, false);

                // O
        	createBox(world, 140, 435, 20, 5, false);
        	createBox(world, 155, 405, 5, 25, false);
       		createBox(world, 125, 405, 5, 25, false);
        	createBox(world, 140, 375, 20, 5, false);

        	// L
       		createBox(world, 200, 435, 20, 5, false);
       		createBox(world, 185, 400, 5, 30, false);

       		// A
       		createBox(world, 240, 410, 5, 30, false);
       		createBox(world, 278, 425, 5, 15, false);
       		createBox(world, 265, 405, 20, 5, false);
       		createBox(world, 280, 390, 5, 10, false);
       		createBox(world, 260, 375, 25, 5, false);

        	// M
        	createBox(world, 390, 355, 40, 5, false);
       		createBox(world, 360, 400, 10, 40, false);
       		createBox(world, 420, 400, 10, 40, false);
       		createBox(world, 390, 400, 5, 40, false);

       		// U
       		createBox(world, 460, 435, 20, 5, false);
       		createBox(world, 445, 405, 5, 30, false);
       		createBox(world, 475, 405, 5, 30, false);

       		// N
                createBox(world, 495, 415, 5, 30, false);
                createBox(world, 525, 415, 5, 30, false);
        	createBox(world, 510, 375, 20, 5, false);

       		// D
       		createBox(world, 558, 435, 18, 5, false);
       		createBox(world, 545, 405, 5, 25, false);
       		createBox(world, 575, 405, 5, 25, false);
       		createBox(world, 558, 375, 18, 5, false);

       		// O
        	createBox(world, 610, 435, 20, 5, false);
        	createBox(world, 595, 405, 5, 25, false);
       		createBox(world, 625, 405, 5, 25, false);
       		createBox(world, 610, 375, 20, 5, false);

       		// !
       		createBox(world, 650, 430, 10, 10, false);
       		createBox(world, 650, 380, 10, 40, false);
            }

Definimos en primer lugar la función createBox(). A partir de 6 parámetros crea una nueva caja en el mundo. En concreto los parámetros son: (1) objeto mundo, (2) coordenada x, (3) coordenada y, (4) ancho, (5) altura, y (6) si tiene o no posición fija.

Su código es prácticamente equivalente al código que usamos para crear el objeto suelo. Además, establecemos la densidad de los objetos que no tienen posición fija, y un coeficiente de rozamiento.

Luego tenemos la función createHelloWorld, que tan sólo crea rectángulos.

Por último, vamos a añadir una llamada a createHelloWorld(); antes de step(), en la función anónima que se ejecuta al cargar la ventana.

Ver demo. La cosa ya va cambiando :) Ahora tenemos ya un montón de cajas, que hasta vemos cómo se tambalean.

Paso 7 – Creando círculos

Nuestro programa ya sabe cómo crear rectángulos en el sistema (createBox()), y cómo dibujarlos en el canvas. Ahora vamos a añadir la opción de crear círculos, y de dibujarlos. Primero, vamos a añadir esta función:

            function createBall(world, x, y) {
                var ballSd = new b2CircleDef();
                ballSd.density = 1.0;
                ballSd.radius = 20;
                ballSd.restitution = 0.5;
                ballSd.friction = 0.5;
                var ballBd = new b2BodyDef();
                ballBd.AddShape(ballSd);
                ballBd.position.Set(x,y);
                return world.CreateBody(ballBd);
            }

Como vemos, es prácticamente igual que createBox(). En este caso al crear el objeto forma, no hemos elegido la clase b2BoxDef, sino b2CircleDef. Y además no tenemos la opción de especificar el tamaño, sino que todas las esferas tendrán el mismo radio, como vemos en “ballSd.radius = 20;”.

El sistema ya sabe añadir y reconocer círculos. Ahora vamos a indicarle cómo dibujarlos. En el switch de la función drawShape() que añadimos en el paso 4, vamos a añadir un nuevo caso:

                    case b2Shape.e_circleShape:
                        {
            				var circle = shape;
            				var pos = circle.m_position;
            				var r = circle.m_radius;
            				var segments = 16.0;
            				var theta = 0.0;
            				var dtheta = 2.0 * Math.PI / segments;
    				        context.moveTo(pos.x + r, pos.y);

    				        for (var i = 0; i <= segments; i++) {
            					var d = new b2Vec2(r * Math.cos(theta), r * Math.sin(theta));
            					var v = b2Math.AddVV(pos, d);
            					context.lineTo(v.x, v.y);
            					theta += dtheta;
                            }
                            context.moveTo(pos.x, pos.y);
                            var ax = circle.m_R.col1;
                            var pos2 = new b2Vec2(pos.x + r * ax.x, pos.y + r * ax.y);
                            context.lineTo(pos2.x, pos2.y);
                        }
                        break;

Ahora en la función drawShape(), en caso de que le proporcionen un círculo, sabrá cómo dibujarlo. En concreto lo hará dividiéndolo en 16 segmentos, y con un poco de trigonometría, se calculará el vector de cada segmento, y sus coordenadas finales.

Además, tras dibujar el círculo, se añadirá un radio, que nos permitirá observar cómo realmente gira el círculo.

Ver demo. Nada nuevo, ya que todavía no hemos creado en ningún momento un círculo.

Paso 8 – Creando objetos con el ratón

Ahora vamos a ver cómo crear nuevos objetos al vuelto, haciendo clic con el ratón. Veremos también cómo impactan con los que ya hay, y cómo efectivamente se ve que se respetan las leyes físicas ;)

Vamos a añadir este código antes de la llamada a step() en la función anónima:

                Event.observe('canvas-world', 'click', function(e) {
                    if (Math.random() > 0.5) {
                        createBox(world, e.clientX, e.clientY, 10, 10, false);
                    } else {
                        createBall(world, Event.pointerX(e), Event.pointerY(e));
                    }
                });

Al hacer clic en el canvas, habrá un 50% de probabilidades de que se cree o bien una caja, o bien un círculo. Lo interesante es que se creará donde hayamos hecho clic, y desde ahí caerá :)

Ver demo final. Si quieres, también puedes descargarte el código completo, con el ejemplo final documentando cada línea.

Por supuesto, ahora se pueden añadir imágenes de fondo al canvas, o añadir sprites a nuestros objetos. Y desde luego también se pueden añadir sonidos. Aquí tienes un ejemplo prácticamente igual que el que acabamos de hacer, pero con sonidos y texturas:

Mejorar el rendimiento de javascript con timers

Uno de mis últimos encargos consiste en una aplicación financiera para el navegador. En resumen, se trata de “traducir” una serie de hojas en Excel a javascript. El cómo lo he implementado da para otro artículo.

Básicamente el funcionamiento es el siguiente:

  • Creamos un campo de texto por cada celda del fichero Excel.
  • Muchas celdas tienen valores dinámicos, es decir, se generan a partir del valor de otras.
  • Definimos la fórmula que genera el valor de cada celda dinámica, y esa fórmula la añadimos a la pila de callbacks del evento ‘change’ de todas las celdas de las que depende.

Problema: Hay miles de celdas, prácticamente todas dinámicas. Esto implica que a menudo, al cambiar un sólo valor, se generan cientos o miles de cálculos. Javascript es single threaded, es decir, sólo se ejecuta una única hebra (para realizar cálculos, modificar el árbol DOM, gestionar eventos, o redibujar la pantalla). Si una operación muy costa en cálculos conlleva 8 segundos, la interfaz de usuario se quedará bloqueada hasta que finalice. Mal asunto.

El experto en usabilidad Jakob Nielsen dijo:

0,1 segundos es el límite para que el usuario perciba que el sistema responde inmediatamente.

¿Son los Web Workers la solución?

Quizás te preguntes qué son los Web Workers. Se trata de una especificación de WHATWG, el equipo que ha dado forma a lo que hoy conocemos como HTML5, para permitir programar aplicaciones multihebra en javascript, es decir, correr procesos en segundo plano.

Si bien es una funcionalidad que han empezando a incorporar recientemente los navegadores (a excepción de IE9), no es nada nuevo; el paquete Google Gears ya ofrecía estos mecanismos. De hecho, los web workers están basados en la API WorkerPool de Google Gears.

Por tanto, ¿son la solución al problema? Sí, pero…

A pesar de que una hoja de cálculo puede ser un ejemplo de libro para el uso de Web Workers, finalmente no los he utilizado. Existen algunas limitaciones, por cuestiones de seguridad, a la hora de usar Web Workers. A destacar, que no tiene acceso a:

  1. DOM.
  2. objeto window.
  3. objeto document.
  4. objeto parent.

Toda la comunicación con la hebra principal se realiza mediante mensajes. Esto interfiere con el código que ya tenía, de modo que implicaría reescribirlo en gran parte. No es una opción por desgracia.

Luego está el motivo de que no funcionan ni funcionarán en Internet Explorer.

Timers al rescate

Repasemos conceptos de sistemas operativos. En un sistema multitarea, en un momento dado no se pueden ejecutar más tareas que procesadores tiene el equipo. Por eso existe el planificador de procesos, que es la parte que se encarga de repartir el tiempo disponible entre los distintos procesos en ejecución. Si se asignan tiempos bajos de ejecución, el efecto será que están funcionando varios procesos concurrentemente. Sintetizando muchísimo, podríamos decir que el planificador de procesos se encarga de asignar bloques de tiempo a cada proceso.

Ahora repasemos los timers. ¿Qué hacen exactamente? Posponen la ejecución de un comando. Esto significa que, mientras se espera ese tiempo, el navegador queda libre para hacer otras cosas.

¿Y si añadimos pausas voluntarias en la propagación de eventos ‘change’ de las celdas? De esta forma, habrá pausas que permitirán “descongelar” la interfaz de usuario, emulando que los cálculos se están ejecutando en un segundo plano, haciendo que la interfaz de usuario sea fluida. Miremos el siguiente gráfico:

Gráfico de relación de tiempos con y sin timers

El color azul representa los cálculos. El verde, es cuando no realizamos cálculos y la interfaz responde a eventos (el equivalente a este proceso en sistemas operativos se llama idle).

En el primer caso no utilizamos timers. Lo que ocurre es que el navegador se queda congelado durante más de tres segundos.

Sin embargo, en el segundo caso añadimos pausas voluntarias, con lo que percibimos que en todo momento el navegador reacciona inmediatamente a nuestras acciones.

Demo

He hecho una sencilla demo. El script se encarga de calcular los números primos menores de 300.000. En un caso con timers y en otro sin él:

  1. Sin timers. La página se quedará congelada hasta que finalice el cálculo.
  2. Con timers. Podrás interactuar con la página mientras hace los cálculos.

Si miramos el código, veremos que hay algunas diferencias. En el primer caso, esto es lo que ocurre:

jQuery("#button").bind('click', function() {
  for (var i=1; i<=300000; i++) {
    if(isPrime(i)) {
      jQuery('#prime').append(" "+ i);
    }
  }

Comprobamos uno a uno cada número desde un bucle, y si es primo, lo mostramos en pantalla. Seguramente haga saltar un aviso en el navegador de que el script está llevando demasiado tiempo.

El segundo ejemplo es un poco más elaborado:

function comprobarPrimo(numero, fin,total) {
  var delay = 10;
  var heap = 500;
  if (isPrime(numero)) {
    jQuery('#prime').append(" "+ numero);
  }
  if(++numero < fin) {
    if (total%heap === 0) {
      setTimeout("comprobarPrimo("+numero+","+fin+","+(total+1)+")", delay);
    } else {
      comprobarPrimo(numero, fin, (total+1));
    }
  }
}

jQuery("#button").bind('click', function() {
  comprobarPrimo(1,300000,0);
});

En lugar de usar un bucle, usamos una función recursiva. Esto lo hacemos porque, cada vez que llamamos a setTimeout, se ejecuta el código que le sigue sin esperar el delay. Es decir, que al añadir un timer no pararíamos realmente toda la ejecución. Con una función recursiva corregimos esto.

Y definimos dos variables: delay, que es el retardo en milisegundos que se aplicará, y heap, que define cada cuántas llamadas se aplica delay. En este ejemplo se establece un delay de 20 milisegundos que se aplica cada 500 números primos. Los otros 499 se ejecutan sin retardo.

El resultado es completamente distinto al del primer ejemplo. Y únicamente añadimos pausas de 20 milisegundos cada 500 números primos. Por supuesto el tiempo de ejecución se ve incrementado.

Programación basada en prototipos

En los resúmenes de los distintos paradigmas de programación se suele mencionar la programación funcional, lógica, procedural, orientada a objetos…

Dentro de la programación orientada a objetos, hay distintos tipos. El más conocido es el basado en clases, donde encontramos lenguajes como Java, C++, C#… Sin embargo, hay más tipos de programación orientada a objetos, y es usada por lenguajes muy conocidos. Me estoy refiriendo a la programación basada en prototipos, y su lenguaje más conocido es JavaScript.

Según la Wikipedia: Programación basada en prototipos es un estilo de programación orientada a objetos en el cual, las “clases” no están presentes, y la re-utilización de procesos (conocida como herencia en lenguajes basados en clases) se obtiene a través de la clonación de objetos ya existentes, que sirven de prototipos, extendiendo sus funcionalidades.

Es decir, se trata de un paradigma orientado a objetos sin clases. ¿Cómo es esto? En lugar de definir una clase, para después crear una instancia de ella, lo que hacemos es definir directamente el nuevo objeto, con sus métodos y atributos. En la página web de Mozilla tenemos un buen resumen de las diferencias existentes entre la POO basada en prototipos y la basada en clases.

Una guía muy buena para entender, paso a paso, en qué consiste, y cómo funciona, es la que proporciona de nuevo Mozilla en su artículo “A re-introduction to JavaScript“, en el apartado “Custom Objects“. Al no estar en español, voy a copiarlos aquí:

Consideremos un objeto persona con los campos nombre y apellido. Hay dos formas de mostrar: “nombre apellido” o “apellido, nombre”. Esta es una forma de hacerlo:

function makePerson(first, last) {
    return {
        first: first,
        last: last
    }
}
function personFullName(person) {
    return person.first + ' ' + person.last;
}
function personFullNameReversed(person) {
    return person.last + ', ' + person.first
}

Esto funciona, pero es bastante feo. Terminas con docenas de funciones y tu espacio de nombres global. Lo que realmente necesitamos es una forma de vincular una función a un objeto. Puesto que las funciones son objetos, esto es fácil:

function makePerson(first, last) {
    return {
        first: first,
        last: last,
        fullName: function() {
            return this.first + ' ' + this.last;
        },
        fullNameReversed: function() {
            return this.last + ', ' + this.first;
        }
    }
}

Podemos aprovechar la palabra clave ‘this‘ para mejorar nuestra función makePerson:

function Person(first, last) {
    this.first = first;
    this.last = last;
    this.fullName = function() {
        return this.first + ' ' + this.last;
    }
    this.fullNameReversed = function() {
        return this.last + ', ' + this.first;
    }
}

Nuestro objeto persona ha mejorado, pero aun tiene algunos flecos feos. Cada vez que creamos un objeto persona, estamos creando dos veces las funciones que hay en cada uno – ¿no sería genial si pudieran compartir este código?

function personFullName() {
    return this.first + ' ' + this.last;
}
function personFullNameReversed() {
    return this.last + ', ' + this.first;
}
function Person(first, last) {
    this.first = first;
    this.last = last;
    this.fullName = personFullName;
    this.fullNameReversed = personFullNameReversed;
}

Esto está mejor: estamos creando las funciones sólo una vez, y asignando referencias a ellas dentro del constructor. ¿Podemos hacerlo todavía mejor? La respuesta es sí:

function Person(first, last) {
    this.first = first;
    this.last = last;
}
Person.prototype.fullName = function() {
    return this.first + ' ' + this.last;
}
Person.prototype.fullNameReversed = function() {
    return this.last + ', ' + this.first;
}

Person.prototype es un objeto compartido por todas las instancias de Person. Forma parte de una cadena de búsqueda (que tiene un nombre especial, “prototype chain”): cada vez que intentas acceder a una propiedad de Person que no está asignada, JavaScript comprobará en Person.prototype para ver si esta propiedad existe ahí. Como resultado, cualquier cosa que asignemos a Person.prototype estará disponible en todas las instancias de ese constructor mediante el objeto this.

Ahora, si os pasaba como a mí, ya tendréis claro qué es eso que hace tan “rarito” a JavaScript. Y se trata ni más ni menos de su forma de ser orientado a objetos.