Las listas de nodos y los Arrays en Javascript

09 Ago 2011

Introducción

Como resulta frecuente en los lenguajes de tipado dinámico, en ocasiones, no es sencillo determinar el tipo de datos de una variable concreta. Ya hemos visto en artículos anteriores como existen algunos objetos que se parecen arrays, se comportan como tales, pero que no lo son. Era el caso por ejemplo del objeto arguments y del que ahora nos sirve de estudio: las listas de nodos (o NodeLists).

Las listas de nodos son uno de los elementos tipo array más utilizados cuando realizamos aplicaciones que implican un trabajo con el DOM y son el resultado de métodos como:

var myNodeLists = document.getElementByTagName('a');

Con el código anterior, estaríamos seleccionando todos los elementos de la página que se correspondan con una etiqueta ‘a‘, es decir, los anchors o enlaces. Es un escenario muy común pero, ¿alguna vez nos hemos parado a estudiar el tipo de datos que nos son devueltos? Es una lista si, pero ¿qué tipo de lista?

Veámos en este artículo qué son exactamente las listas de nodos y cómo podemos manejarlas de una forma cómoda y sin equívocos.

NodeLists

Según la definición del W3C, una lista de nodos es una interfaz que proporciona la abstracción de una colección ordenada de nodos sin definir o limitar cómo está implementada.

Un aspecto importante a tener en cuenta es que, como cuaquier otro método que interactúa con el DOM, las listas de nodos no pertenecen a la especificación Javascript, sino a la API que los navegadores proporcionan para que Javascript interactúe con el modelo de objetos en el documento. Este detalle es clave para entender la naturaleza de aquellos datos que obtenemos mediante el uso de dicha API. Volveremos sobre esto un poco más adelante.

NodeLists y Arrays

Si volvemos a la definición dada por el W3C, tenemos el hecho de que una lista de nodos es una colección ordenada de elementos. Esto recuerda de forma directa al concepto de arrays; de hecho las similitudes son muchas.

Tomemos por ejemplo la portada de la Wikipedia en español y busquemos todas las etiquetas anchor como hicimos en el ejemplo de más arriba:

var myNodeLists = document.getElementsByTagName('a');
console.log( myNodeLists );

Con lo anterior, tendremos dentro de la variable myNodeLists todas los elementos del DOM que pertenezcan a la etiqueta seleccionada.

Probemos ahora a obtener el número total de elementos del mismo modo en que lo haríamos con un array normal:

console.log( myNodeLists.length ); // 384

Si preguntamos por el contenido del objeto, obtenemos en este caso el número 384: esos los nodos que coinciden con el criterio de selección. El método length funciona como se espera; sin embargo, tratemos por ejemplo de invocar otro como por ejemplo el slice:

myNodeLists.slide( 5 ); // ERROR

No funciona. Empiezan aquí las distinciones entre la lista de nodos y los arrays convencionales. Entonces, si no es un array, ¿con qué tipo de objeto estamos tratando? Preguntémosle al propio intérprete Javascript:

console.log( typeof myNodeLists ); // object
console.log( myNodeLists instanceof Array ); // false
myNodeLists.constructor.toString(); // "function Object() { [native code] }"

Vaya; es un objeto, pero no una matriz convencional: se trata de un tipo concreto que no pertenece al estándar ECMAScript.

Si lo comparamos con un verdadero array, vemos las diferencias:

var myArr = [1, 2];
console.log( typeof myArr ); // object
console.log( myArr instanceof Array ); // true
myArr.constructor.toString(); // "function Array() { [native code] }"

Un array es, efectivamente, un objeto e instancia de la primitiva Array que utiliza lógicamente este constructor. Por lo que, si dos elementos tienen constructores diferentes, resulta lógico que no compartan sus mismos métodos.

Convirtiendo la lista de nodos en un array

Si necesitamos trabajar con nuestra lista de nodos como si de un array genuino se tratase, podemos utilizar el mismo truco que con el objeto arguments:

var myArr = Array.prototype.slice.call(myNodeLists, 0);

Y ahora si; una simple comprobación nos revela que ahora tenemos el objeto esperado:

myArr.constructor.toString(); // "function Array() { [native code] }"

La excepción: IE

Como no podía ser de otro modo, tenemos excepciones: en Internet Explorer, el método slice no funciona sobre determinados objetos y éste, es uno de ellos. Para corregir esto, debemos crear un nuevo array, iterar por nuestra lista de nodos e ir insertando cada valor en el correspondiente índice:

var myIEArray = [];
for (var i = 0; i < myNodeLists.length; ++i) { myIEArray.push( myNodeLists[ i ] ); }
myIEArray.constructor.toString(); function Array() { [native code] }

Tenemos nuestro array, ¿y ahora qué?

Cabe mencionar que cuando convertimos una lista de nodos en un array, dejamos de trabajar con elementos vivos del DOM; en su lugar, tenemos una lista estática de nodos. Esto quiere decir que si utizamos métodos como pop, no veremos desaparecer los nodos en el navegador a medida que se van ejecutando las instrucciones sino que actuamos sobre un array que referencia al nodo pero que no interactúa con él. Sin embargo, y esto es la curiosidad importante, el valor de esos nodos si están enlazados con el DOM. Esto último quiere decir que si hacemos algo como:

myArr[0].firstChild.data = "Hello World";

el contenido de nuestra etiqueta si cambia!. Esto sin duda, puede ser una funcionalidad interesante para aplicaciones complejas que exigen de una intensa manipulación del DOM.

¿Y qué pasa con jQuery?

Es cierto que dada la popularidad de algunas bibliotecas como jQuery, pocos utilizan ya el API DOM para seleccionar elementos. En su lugar, solemos recurrir a los métodos que nos ofrecen estas herramientas:

var theAnchorsList = $('a');

Mucho más sencillo, rápido e intuitivo que el document.getElementsByTagName. Pero, ¿y este nuevo objeto? ¿qué es?

Preguntémosle sin más:

console.log( typeof theAnchorsList ); // object
console.log( theAnchorsList instanceof Array ); // false
theAnchorsList.constructor.toString(); // "function Object() { [native code] }"

Tampoco es un array!. A día de hoy (versión 1.6.2), jQuery no realiza ninguna conversión del resultado, por lo que volvemos al mismo escenario anterior.

Sin embargo, si echamos un vistazo a la biblioteca ZeptoJS, la jQuery minificada, encontramos que en ella si se hace una conversión explícita del objeto resultante de un selector en array para su posterior manipulación:

$.qsa = $$ = function(element, selector){ return slice.call(element.querySelectorAll(selector)) }

Además del uso del element.querySelectorAll, vemos como directamente implementa el slice.call. Sin duda, esto supone una pequeña ventaja con respecto a la manipuación que permite jQuery. Veremos cuánto tardan en implementarla!

Más información

Can Duruk, NodeLists and Arrays in JavaScript
W3C, Document Object Model Core
James Edwards, A collection is not an array
John David Dalton, Test de rendimiento de iteraciones en listas de nodos VS arrays (JSPerf).

Más:

{2} Comentarios.

  1. gnz/vnk

    jQuery no implementará esto, IMHO. El API de jQuery ya tiene una función, makeArray, que puede servirte para pasar una NodeList a un Array… si necesitas hacerlo. Porque si no lo vas a necesitar (como es en la enorme mayoría de casos) ¿por qué obligar a su uso incurrieno en la penalización de rendimiento que puede suponer?

  2. rafael8a

    A la 1ra entendi todo (menos el ultimo codigo).
    Y no me gusta programar.
    Creo q soy un caso psiquiatrico.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *