Prototype: iteradores

Visita este artí­culo en http://www.estadobeta.com/2006/09/04/prototype-iteradores/

Por Ismael en artículos, javascript

Sumario de los métodos iteradores introducidos por Prototype.js

Disclaimer inicial: este artículo está criminalmente inspirado en Prototype Meets Ruby: A Look at Enumerable, Array and Hash, de Justin Palmer.

Prototype es una librería Javascript que agrega utilidades al lenguaje para escribir aplicaciones más complejas con menos código. Si no lo has hecho, te recomiendo que leas el artículo introductorio aquí en EstadoBeta antes de continuar con este artículo.

Prototype implementa métodos iteradores, basandose en los iteradores nativos de Ruby. Los iteradores son métodos que permiten loopear - recorrer - colecciones como arrays y objetos de manera más intuitiva, además de agregar funcionalidad nueva. Primero que nada, definamos un grupo de arrays y hashes que usaremos en los ejemplos:


var C = {
  Productos: [
    {nombre: 'Ipod', marca: 'Apple',  tipo: 'Reproductor MP3},
    {nombre: 'Coolpix',  marca: 'Fuji', tipo: 'Cámara Digital'},
    {nombre: 'Learning Ruby', marca: 'TPP', tipo: 'Libro'}
  ],
  Musicos:   ['Sonic Youth', 'Tim Buckley', 'The 6ths', 'The Beatles', 'Aluminium Group'],
  Numeros:  [0, 1, 4, 5, 98, 32, 12, 9]

};


C es un objeto que posee tres arrays: C.Productos es un array de objetos literales Javascript. Cada objeto representa a un producto y tiene los atributos nombre, marca y tipo.
C.Musicos es un array simple con los nombres de músicos y bandas. C.Numeros es eso, un array de números.

each y sus amigos

Normalmente usamos los loops nativos de Javascript para recorrer colecciones de este tipo:


for(var i = 0; i < C.Numeros.length; i++) {
  document.write( C.Numeros[i] );
}

Aunque Prototype no elimina la necesidad de los loops, el método each() y sus parientes introducen una forma más limpia de recorrer colecciones.


C.Numeros.each( function(num){
    document.write( num );
});

//resultado:
0 1 4 5 98 32 12 9

each() recibe un argumento, el iterador. El iterador - o “bloque” en Ruby - es una función que es ejecutada una vez por cada elemento del array. El elemento de turno es pasado a la función iteradora como argumento. En el cuerpo del iterador podemos hacer lo que queramos con cada elemento. Opcionalmente, each() pasa el index del elemento correspondiente como segundo argumento del iterador.


C.Numeros.each( function(num, index){
    document.write( index + “=> ” + num +”<br />” );
});

//resultado:
0=> 0
1=> 1
2=> 4
3=> 5
4=> 98
5=> 32
6=> 12
7=> 9

Hash: pares key/value

Los hashes, creados al pasar un objeto javascript a la función $H(), sirven para exponer los atributos del objeto de forma iterable.


$H(C.Productos[0]).each(function(product) {
      document.write( product.key + ": " + product.value +"<br />" );
    });

//resultado:
nombre: Ipod
marca: Apple
tipo: Reproductor MP3

También podemos acceder a los atributos y valores de un hash sin iterar sobre él.


$H(C.Productos[1]).keys();
//Retorna nombre,marca,tipo 

$H(C.Productos[1]).values();
//Retorna Coolpix,Fuji,Cámara Digital

this dentro de los iteradores

Un problema con los iteradores es que se pierde la referencia original a this, el objeto donde se ejecuta el loop. Dentro de la función iteradora, this pasa a ser una referencia a la función. Para remediar esto, Protoype introduce el método bind(), que vuelve el foco de this al objeto o función original.


C.Numeros.each(function(num, index) {
  this.otroMetodo( num );
}.bind(this));

collect

Aquí las cosas se ponen interesantes. collect() permite iterar una colección, como each(), pero además retorna los resultados como un nuevo array.


var marcas = C.Productos.collect( function(product) {
  return product.marca;
});

// Retorna el nuevo array [Apple, Fuji, TPP]

El resultado es un array javascript, que acepta los métodos nativos de Array.


C.Productos.collect( function(product) {
  return product.marca;
}).join(”, “);

// Escribe Apple, Fuji, TPP

include

include() verifica que el argumento pasado exista en la colección, retornando true si es así o false en caso contrario. Para verificar que un músico existe en mi array de músicos:


 return C.Musicos.include(’Daddy Yankee’); // retorna false, por supuesto!

inject

inject() sirve para sumar los elementos de un array.


var score = C.Numeros.inject(0, function(suma, value) {
  return suma + value;
});
document.write( score );

//Escribe 161

El primer argumento es un valor inicial para sumar. Si pasáramos 1 en lugar de 0, el resultado final sería 162.

findAll

findAll() retorna un nuevo array con los elementos donde el iterador evalua a true.


var productos_apple = C.Productos.findAll(function(product) {
  return product.marca == 'Apple';
});
document.write( productos_apple[0].nombre );

//Escribe Ipod

detect

A diferencia de findAll(), detect() retorna sólo el primer elemento donde el iterador evalua a true. Así, si quiero encontrar el primer número de C.Numeros que es mayor a 5:


var n = C.Numeros.detect(function(num) {
  return num > 5
});
document.write( n );

//Escribe 98

Aunque hay más números mayores a 5 en nuestro array, detect() nos entrega sólo el primero.

pluck

pluck() recibe un string como argumento, y retorna un nuevo array con todos los valores del array original si los elementos son objetos y tienen un atributo llamado como el string.


var nombres = C.Productos.pluck( "nombre" );

//retorna nuevo array ["Ipod", "Coolpix", "Programming Ruby" ];

Esto nos permite escribir código elegante como este:


C.Productos.pluck( “nombre” ).each( function(nombre, index){
    document.write( “Nombre “+(index+1)+” => “+nombre+”<br />” );
});

//Escribe:
Nombre 1 => Ipod
Nombre 2 => Coolpix
Nombre 3 => Programming Ruby

$A()

Finalmente, hay una serie de colecciones nativas de Javascript que no soportan los mismos métodos que Array. Este es el caso de las colecciones de elementos DOM - por ejemplo el resultado de document.getElementsByTagName() -. Para estos casos basta con pasar la colección a la función $A(), que convertirá la colección en un array que podemos iterar con los métodos vistos en este artículo.


$A(document.getElementsByTagName(’tr’)).each(…);

each(): clases iterables

Esto puede ser sabrosón para ustedes que estan familiarizados con los conceptos OOP de clases e interfaces. En Prototype, los métodos iteradores each() y familia pertenecen al módulo Enumerable*1. Si nuestra clase hereda de Enumerable e implementa el método especial _each() (notese el guión bajo), each() y todos sus amigos estarán disponibles para nuestra clase. El siguiente ejemplo representa un reproductor de canciones e implementa métodos para agregar y tocar canciones, además del método especial _each() patra hacer la colección iterable.


var Reproductor = Class.create();
Object.extend( Object.extend(Reproductor.prototype, Enumerable),{
    initialize:function(){
        this.canciones = []; //colección interna de canciones
    },
    addSong: function( el_titulo,el_autor ){
        var cancion = {titulo:el_titulo, autor:el_autor};
        this.canciones.push( cancion );
    },
    _each: function( iterador ) { //implementa métodos de Enumerable
        this.canciones.each( iterador );
    }
}); 

//Ejemplo:
var R = new Reproductor();
R.addSong( “Ill do it my way”, “Frank Sinatra” );
R.addSong( “Let it be”, “Beatles” );
R.addSong( “Material Girl”, “Madonna” );

R.each( function(cancion){
    document.write( cancion.autor + “, ” );
});
//Escribe: Frank Sinatra, Beatles, Madonna

var titulos = R.collect( function(cancion){return cancion.titulo} );
//retorna nuevo array [”Ill do it my way”, “Let it be”, “Material Girl”]

var una_cancion = R.detect( function(cancion){cancion.titulo = “Let it be”} );
document.write( una_cancion.autor );
//Escribe “Beatles”

Veremos más de esta poderosa utilidad de Prototype en los próximos artículos de esta serie.

*1
Javascript no soporta módulos a la manera de Ruby, sino que los implementa en objetos globales que pueden ser incorporados a nuestros propios objetos usando Object.extend().

Links

prototype.js, javascript, enumerable, iterators

4 comentarios para “Prototype: iteradores”

  1. GravatarPrototype y los iteradores - aNieto2K Dice:

    […] Ismael de EstadoBeta se ha currado un manual con el cual podremos manejar los nuevos iteradores que nos aporta como novedad el nuevo prototype. Un manual excelente para todos los que queramos aprender a usar esta librería. […]

  2. Gravatarmarkdbd Dice:

    Muy interesante gracias por el tutorial :)

  3. GravatarEstadoBeta » Archivo » Prototype: Ajax Dice:

    […] Hemos visto suficiente de la librería Javascript Prototype como para entrar de lleno en una de sus utilidades más… Estee… útiles: Ajax (si no sabes qué es Prototype lee este y este artículo). […]

  4. Gravatarjgrdal Dice:

    Excelente artículo. Muy bien explicado todo.