Pourquoi document.querySelectorAll renvoie-t-il un StaticNodeList plutôt qu'un vrai tableau?

103

Cela me dérange que je ne puisse pas simplement faire document.querySelectorAll(...).map(...)même dans Firefox 3.6, et je ne trouve toujours pas de réponse, alors j'ai pensé que je posterais sur SO la question de ce blog:

http://blowery.org/2008/08/29/yay-for-queryselectorall-boo-for-staticnodelist/

Est-ce que quelqu'un connaît une raison technique pour laquelle vous n'obtenez pas de tableau? Ou pourquoi ne StaticNodeList pas hérité d'un tableau de manière à ce que vous pouvez utiliser map, concatetc?

(BTW si c'est juste une fonction que vous voulez, vous pouvez faire quelque chose comme NodeList.prototype.map = Array.prototype.map;... mais encore une fois, pourquoi cette fonctionnalité est-elle (intentionnellement?) Bloquée en premier lieu?)

Kev
la source
3
En fait, getElementsByTagName ne renvoie pas non plus un Array, mais une collection, et si vous voulez l'utiliser comme un Array (avec des méthodes comme concat, etc.), vous devez convertir une telle collection en un Array en faisant une boucle et en copiant chaque élément du collection dans un tableau. Personne ne s'est jamais plaint de cela.
Marco Demaio

Réponses:

81

Je pense que c'est une décision philosophique du W3C. La conception du DOM W3C [spec] est assez orthogonale à la conception de JavaScript, car le DOM est censé être neutre en termes de plate-forme et de langage.

Les décisions telles que " getElementsByFoo()retourne un ordre NodeList" ou " querySelectorAll()retourne un StaticNodeList" sont très intentionnelles, de sorte que les implémentations n'ont pas à se soucier d'aligner leur structure de données retournée en fonction des implémentations dépendantes du langage (comme .mapêtre disponible sur les tableaux en JavaScript et Ruby, mais pas sur les listes en C #).

Le W3C vise bas: ils diront que a NodeListdevrait contenir une propriété en lecture seule .lengthde type unsigned long parce qu'ils pensent que chaque implémentation peut au moins prendre en charge cela , mais ils ne diront pas explicitement que l' []opérateur d'index devrait être surchargé pour prendre en charge l'obtention d'éléments positionnels, parce qu'ils ne veulent pas contrecarrer un petit langage pauvre qui veut être implémenté getElementsByFoo()mais ne peut pas supporter la surcharge des opérateurs. C'est une philosophie répandue présente dans une grande partie de la spécification.

John Resig a exprimé une option similaire à la vôtre, à laquelle il ajoute :

Mon argument n'est pas tant qu'il NodeIteratorne ressemble pas beaucoup à DOM, c'est qu'il n'est pas très semblable à JavaScript. Il ne tire pas parti des fonctionnalités présentes dans le langage JavaScript et les utilise au mieux de ses capacités ...

Je compatis quelque peu. Si le DOM était écrit spécifiquement avec les fonctionnalités JavaScript à l'esprit, il serait beaucoup moins gênant et plus intuitif à utiliser. En même temps, je comprends les décisions de conception du W3C.

Croissant frais
la source
Merci, cela m'aide à comprendre la situation.
Kev
@Kev: J'ai vu votre commentaire sur cette page d'article de blog vous demandant comment vous feriez pour convertir le StaticNodeListen tableau. J'approuverais la réponse de @ mck89 comme la voie à suivre pour convertir un NodeList/ StaticNodeListen un tableau natif, mais cela échouera dans IE (8 obv) avec une erreur JScript, puisque ces objets sont hébergés / "spéciaux".
Crescent Fresh
C'est vrai, c'est pourquoi je l'ai voté. Cependant, quelqu'un d'autre a annulé mon +1. Qu'entendez-vous par hébergé / spécial?
Kev
1
@Kev: les variables hébergées sont toutes les variables fournies par l'environnement "hôte" (par exemple un navigateur Web). Par exemple document, window, etc. IE implémente souvent ces « spécialement » (par exemple sous forme d' objets COM) qui parfois ne sont pas conformes à une utilisation normale, de manière petites et subtiles, telles que le Array.prototype.slice.callbombardement quand donné StaticNodeList;)
Croissant frais
201

Vous pouvez utiliser l' opérateur de propagation ES2015 (ES6) :

[...document.querySelectorAll('div')]

convertira StaticNodeList en tableau d'éléments.

Voici un exemple d'utilisation.

[...document.querySelectorAll('div')].map(x => console.log(x.innerHTML))
<div>Text 1</div>
<div>Text 2</div>

Vlad Bezden
la source
24
Une autre façon est d'utiliser Array.from () :Array.from(document.querySelectorAll('div')).map(x => console.log(x.innerHTML))
Michael Berdyshev
42

Je ne sais pas pourquoi il renvoie une liste de nœuds au lieu d'un tableau, peut-être parce que, comme getElementsByTagName, il mettra à jour le résultat lorsque vous mettez à jour le DOM. Quoi qu'il en soit, une méthode très simple pour transformer ce résultat en un tableau simple est:

Array.prototype.slice.call(document.querySelectorAll(...));

et ensuite vous pouvez faire:

Array.prototype.slice.call(document.querySelectorAll(...)).map(...);
mck89
la source
3
En fait, il ne met pas à jour le résultat lorsque vous mettez à jour le DOM - d'où «statique». Vous devez à nouveau appeler manuellement qSA pour mettre à jour le résultat. +1 pour la sliceligne cependant.
Kev
1
Ouais, comme Kev l'a dit: le jeu de résultats qSA est statique, le jeu de résultats getElementsByTagName () est dynamique.
joonas.fi
IE8 ne prend en charge que querySelectorAll () en mode standard
mbokil
13

Juste pour ajouter à ce que Crescent a dit,

si vous ne voulez qu'une seule fonction, vous pouvez faire quelque chose comme NodeList.prototype.map = Array.prototype.map

Ne fais pas ça! Il n'est pas du tout garanti de fonctionner.

Aucun standard JavaScript ou DOM / BOM ne spécifie que la NodeListfonction constructeur existe même en tant que windowpropriété globale / , ou que le NodeListretourné par querySelectorAllen héritera, ou que son prototype est accessible en écriture, ou que la fonction fonctionnera Array.prototype.mapréellement sur une NodeList.

Un NodeList est autorisé à être un «objet hôte» (et en est un, dans IE et certains navigateurs plus anciens). Les Arrayméthodes sont définies comme étant autorisées à fonctionner sur n'importe quel 'objet natif' JavaScript qui expose des valeurs numériques et des lengthpropriétés, mais elles ne sont pas obligées de travailler sur des objets hôtes (et dans IE, ce n'est pas le cas).

C'est ennuyeux que vous n'obteniez pas toutes les méthodes de tableau sur les listes DOM (toutes, pas seulement StaticNodeList), mais il n'y a pas de moyen fiable de contourner cela. Vous devrez convertir manuellement chaque liste DOM que vous récupérez en un tableau:

Array.fromList= function(list) {
    var array= new Array(list.length);
    for (var i= 0, n= list.length; i<n; i++)
        array[i]= list[i];
    return array;
};

Array.fromList(element.childNodes).forEach(function() {
    ...
});
bobince
la source
1
Shoot, je n'y ai pas pensé. Merci!
Kev
Je suis d'accord +1. Juste un commentaire, je pense que faire "var array = []" au lieu de "var array = new Array (list.length)" rendra le code encore plus court. Mais je suis intéressé si vous savez qu'il pourrait y avoir un problème à faire cela.
Marco Demaio
@MarcoDemaio: Non, pas de problème. new Array(n)donne juste au terp JS un indice sur la durée du tableau. Cela pourrait lui permettre d'allouer cette quantité d'espace à l'avance, ce qui entraînerait potentiellement une accélération car certaines réallocations de mémoire pourraient être évitées à mesure que le tableau se développe. Je ne sais pas si cela aide réellement les navigateurs modernes ... Je soupçonnerais pas mesurable.
bobince
2
Maintenant, il est implémenté dans Array. De ()
Michael Berdyshev
2

Je pense que vous pouvez simplement suivre

Array.prototype.map.call(document.querySelectorAll(...), function(...){...});

Cela fonctionne parfaitement pour moi

Max Leps
la source
0

C'est une option que je voulais ajouter à la gamme d'autres possibilités suggérées par d'autres ici. Il est destiné uniquement au plaisir intellectuel et n'est pas conseillé .


Juste pour le plaisir , voici un moyen de "forcer" querySelectorAllà s'agenouiller et à s'incliner devant vous:

Element.prototype.querySelectorAll = (function(QSA){
    return function(){
        return [...QSA.call(this, arguments[0])]
    }
})(Element.prototype.querySelectorAll);

Maintenant, ça fait du bien de passer à travers cette fonction, en lui montrant qui est le patron. Maintenant, je ne sais pas ce qui est mieux, créer un tout nouveau wrapper de fonction nommée , puis faire en sorte que tout votre code utilise ce nom étrange (à peu près de style jQuery) ou remplace la fonction comme ci-dessus une fois pour que le reste de votre code puisse toujours pour utiliser le nom de la méthode DOM d'origine querySelectorAll.

  • Une telle approche éliminerait l'utilisation possible de sous-méthodes

Je ne le recommanderais en aucun cas, à moins que vous ne donniez honnêtement un [vous savez quoi].

vsync
la source