javascript - De ce returnează document.querySelectorAll un StaticNodeList în loc de un Array adevărat?


Mă deranjează că nu pot pur și simplu să fac document.querySelectorAll(...).map(...) nici măcar în Firefox 3.6, și tot nu pot găsi un răspuns, așa că m-am gândit să postez și pe SO întrebarea de pe acest blog:

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

Știe cineva un motiv tehnic pentru care nu primești un Array? Sau de ce un StaticNodeList nu moștenește de la un Array într-un mod în care ai putea folosi map, concat etc?

(Apropo, dacă vrei doar o funcție, poți face ceva de genul NodeList.prototype.map=Array.prototype.map;...dar din nou, de ce este această funcționalitate blocată (intenționat?) în primul rând?)

Răspuns #1

Poți folosi operatorul spread din ES2015 (ES6):

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

va converti StaticNodeList într-un Array de elemente.

Iată un exemplu despre cum să-l folosești.

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

Răspunsul #2

Cred că este o decizie filozofică a W3C. Designul specificației W3C DOM este destul de ortogonal față de designul JavaScript, deoarece DOM-ul este intenționat să fie neutru din punct de vedere al platformei și al limbajului.

Decizii precum „getElementsByFoo() returnează un NodeList ordonat” sau „querySelectorAll() returnează un StaticNodeList” sunt foarte intenționate, astfel încât implementările să nu fie nevoite să se preocupe de alinierea structurii de date returnate pe baza implementărilor dependente de limbaj (cum ar fi .map disponibil pe Array-uri în JavaScript și Ruby, dar nu pe List-uri în C#).

W3C își propune să nu exagereze: vor spune că un NodeList ar trebui să conțină o proprietate doar pentru citire .length de tip unsigned long, pentru că ei cred că orice implementare poate cel puțin să suporte asta, dar nu vor spune în mod explicit că operatorul de indexare [] ar trebui suprascris pentru a permite obținerea elementelor după poziție, pentru că nu vor să împiedice vreun limbaj mai mic care ar dori să implementeze getElementsByFoo() dar nu poate suporta suprascrierea operatorilor. Este o filosofie răspândită prezentă în mare parte a specificației.

John Resig a exprimat o opinie similară cu a ta, la care adaugă:

Argumentul meu nu este atât de mult că NodeIterator nu este foarte asemănător cu DOM, ci că nu este foarte asemănător cu JavaScript. Nu profită de funcționalitățile prezente în limbajul JavaScript și de a le folosi la maximum din potențialul lor...

Chiar empatizez în oarecare măsură. Dacă DOM-ul ar fi fost scris special având în vedere funcționalitățile JavaScript, ar fi mult mai puțin stângaci și mai intuitiv de utilizat. În același timp, înțeleg deciziile de proiectare ale W3C.

Răspunsul #3

Nu știu de ce returnează o listă de noduri în loc de un tablou, poate pentru că, la fel ca getElementsByTagName, va actualiza rezultatul atunci când actualizezi DOM-ul. Oricum, o metodă foarte simplă de a transforma acel rezultat într-un tablou simplu este:

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

și apoi poți să faci:

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

Răspunsul #4

Doar pentru a adăuga la ce a spus Crescent,

dacă vrei doar o funcție, poți face ceva de genul NodeList.prototype.map=Array.prototype.map

Nu face asta! Nu este deloc garantat că va funcționa.

Niciun standard JavaScript sau DOM/BOM nu specifică faptul că funcția constructor NodeList există măcar ca proprietate globală/window, sau că NodeList returnat de querySelectorAll va moșteni de la acesta, sau că prototipul său este scriabil, sau că funcția Array.prototype.map va funcționa efectiv pe un NodeList.

Un NodeList este permis să fie un „obiect gazdă” (și este unul, în IE și în unele browsere mai vechi). Metodele Array sunt definite ca fiind permise să opereze pe orice „obiect nativ” JavaScript care expune proprietăți numerice și length, dar nu sunt obligate să funcționeze pe obiectele gazdă (și în IE, nu funcționează).

Este enervant că nu poți folosi toate metodele de array pe listele DOM (toate, nu doar StaticNodeList), dar nu există o soluție fiabilă pentru asta. Va trebui să convertești manual fiecare listă DOM pe care o primești într-un Array:

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() {
    ...
});

Răspunsul #5

Array.from(document.querySelectorAll(...)).map(...)

Totuși, nu este disponibil pe IE11 https://caniuse.com/mdn-javascript_builtins_array_from