Dlaczego document.querySelectorAll zwraca StaticNodeList zamiast prawdziwej tablicy?

103

Wkurza mnie to, że nie mogę po prostu zrobić document.querySelectorAll(...).map(...)nawet w Firefoksie 3.6, a nadal nie mogę znaleźć odpowiedzi, więc pomyślałem, że napiszę na SO pytanie z tego bloga:

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

Czy ktoś zna techniczny powód, dla którego nie masz tablicy? Albo dlaczego StaticNodeList nie dziedziczą tablicę w taki sposób, że można użyć map, concatitp?

(Przy okazji, jeśli to tylko jedna funkcja, którą chcesz, możesz zrobić coś takiego NodeList.prototype.map = Array.prototype.map;... ale znowu, dlaczego ta funkcja jest (celowo?) Blokowana w pierwszej kolejności?)

Kev
źródło
3
W rzeczywistości również getElementsByTagName nie zwraca Array, ale kolekcję, a jeśli chcesz używać jej jak Array (z metodami takimi jak concat itp.), Musisz przekonwertować taką kolekcję na Array, wykonując pętlę i kopiując każdy element kolekcja do Array. Nikt nigdy na to nie narzekał.
Marco Demaio

Odpowiedzi:

82

Uważam, że to filozoficzna decyzja W3C. Projekt W3C DOM [specyfikacja] jest dość ortogonalny w stosunku do projektu JavaScript, ponieważ DOM ma być neutralny pod względem platformy i języka.

Decyzje takie jak „ getElementsByFoo()zwraca uporządkowaną NodeList” lub „ querySelectorAll()zwraca StaticNodeList” są bardzo celowe, więc implementacje nie muszą się martwić o wyrównywanie zwracanej struktury danych na podstawie implementacji zależnych od języka (np. .mapSą dostępne w tablicach w JavaScript i Ruby, ale nie na listach w C #).

Niski Celem W3C: oni powiedzieć NodeListpowinna zawierać readonly .lengthwłaściwość typu unsigned long , ponieważ wierzą, przynajmniej wsparcie każdy może implementacja że , ale nie powie wyraźnie, że []operator indeks powinien być przeładowany do obsługi coraz elementy pozycyjne, ponieważ nie chcą przeszkadzać jakimś słabym, małym językowi, który się pojawia, który chce zaimplementować, getElementsByFoo()ale nie może obsługiwać przeciążenia operatora. Jest to powszechna filozofia obecna w większości specyfikacji.

John Resig wypowiedział podobną opcję jak twoja, do której dodaje :

Mój argument nie jest taki, że NodeIteratornie jest bardzo podobny do DOM, ale że nie jest bardzo podobny do JavaScript. Nie korzysta z funkcji dostępnych w języku JavaScript i wykorzystuje je najlepiej, jak potrafi ...

Trochę współczuję. Gdyby DOM został napisany specjalnie z myślą o funkcjach JavaScript, byłby o wiele mniej niewygodny i bardziej intuicyjny w użyciu. Jednocześnie rozumiem decyzje projektowe W3C.

Crescent Fresh
źródło
Dzięki, to pomaga mi zrozumieć sytuację.
Kev
@Kev: Widziałem twój komentarz na tej stronie z artykułem na blogu, w którym kwestionowałbyś, w jaki sposób możesz przekonwertować StaticNodeListtablicę na tablicę. Poparłbym odpowiedź @ mck89 jako sposób konwersji NodeList/ StaticNodeListdo natywnej tablicy, ale to się nie powiedzie w IE (8 obv) z błędem JScript, ponieważ te obiekty są hostowane / „specjalne”.
Crescent Fresh
To prawda, dlatego go poparłem. Jednak ktoś inny anulował moje +1. Co masz na myśli mówiąc „hostowane / specjalne”?
Kev
1
@Kev: zmienne hostowane to dowolne zmienne dostarczane przez środowisko "hosta" (np. Przeglądarkę internetową). Na przykład document, windowitp IE często wykonuje te „sposób” (na przykład jako COM obiektów), które czasami nie są zgodne z normalnego użytkowania, w małych i subtelne sposoby, takie jak Array.prototype.slice.callbombardowanie po poddaniu StaticNodeList;)
Crescent Świeży
201

Możesz użyć operatora rozproszenia ES2015 (ES6) :

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

przekonwertuje StaticNodeList na Array of items.

Oto przykład, jak go używać.

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

Vlad Bezden
źródło
24
Innym sposobem jest użycie Array.from () :Array.from(document.querySelectorAll('div')).map(x => console.log(x.innerHTML))
Michael Berdyshev
42

Nie wiem, dlaczego zwraca listę węzłów zamiast tablicy, może dlatego, że podobnie jak getElementsByTagName zaktualizuje wynik, gdy zaktualizujesz DOM. W każdym razie bardzo prostą metodą przekształcenia wyniku w prostą tablicę jest:

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

a potem możesz:

Array.prototype.slice.call(document.querySelectorAll(...)).map(...);
mck89
źródło
3
W rzeczywistości nie aktualizuje wyniku, gdy aktualizujesz DOM - stąd „statyczny”. Musisz ponownie ręcznie zadzwonić do qSA, aby zaktualizować wynik. sliceJednak +1 za linię.
Kev
1
Tak, jak powiedział Kev: zestaw wyników qSA jest statyczny, zestaw wyników getElementsByTagName () jest dynamiczny.
joonas.fi
IE8 obsługuje tylko querySelectorAll () w trybie standardowym
mbokil
13

Aby dodać do tego, co powiedział Crescent,

jeśli to tylko jedna funkcja, którą chcesz, możesz zrobić coś takiego jak NodeList.prototype.map = Array.prototype.map

Nie rób tego! To wcale nie jest gwarantowane.

Żaden standard JavaScript ani DOM / BOM nie określa, że NodeListfunkcja-konstruktora istnieje nawet jako windowwłaściwość global / lub że NodeListzwrócona przez querySelectorAllbędzie dziedziczyć po niej, lub że jej prototyp jest zapisywalny, lub że funkcja ta Array.prototype.mapbędzie faktycznie działać na NodeList.

NodeList może być „obiektem hosta” (i jest nim w IE i niektórych starszych przeglądarkach). Te Arraymetody są zdefiniowane jako dopuszczone do działania na „kod JavaScript rodzimej obiektu”, który eksponuje numeryczny i lengthwłaściwości, ale nie są one wymagane do pracy na obiektach gospodarza (i w IE, ale nie).

Irytujące jest to, że nie dostajesz wszystkich metod tablicowych na listach DOM (wszystkie, nie tylko StaticNodeList), ale nie ma niezawodnego sposobu na obejście tego. Będziesz musiał ręcznie przekonwertować każdą listę DOM, którą wrócisz, do tablicy:

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
źródło
1
Strzelaj, nie pomyślałem o tym. Dzięki!
Kev
Zgadzam się +1. Tylko komentarz, myślę, że wykonanie „var array = []” zamiast „var array = new Array (list.length)” sprawi, że kod będzie jeszcze krótszy. Ale jestem zainteresowany, jeśli wiesz, że może to być problem.
Marco Demaio
@MarcoDemaio: Nie, nie ma problemu. new Array(n)po prostu daje terpowi JS wskazówkę, jak długo skończy się tablica. To mogłoby pozwolić mu na przydzielenie tej ilości miejsca z wyprzedzeniem, co potencjalnie mogłoby spowodować przyspieszenie, ponieważ można by uniknąć niektórych realokacji pamięci w miarę wzrostu tablicy. Nie wiem, czy to rzeczywiście pomaga w nowoczesnych przeglądarkach ... Podejrzewałbym, że nie jest to wymierne.
bobince
2
Teraz jest zaimplementowany w Array.from ()
Michael Berdyshev
2

Myślę, że możesz po prostu śledzić

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

U mnie działa idealnie

Max Leps
źródło
0

Jest to opcja, którą chciałem dodać do wachlarza innych możliwości sugerowanych przez innych tutaj. Jest przeznaczony wyłącznie do intelektualnej zabawy i nie jest zalecany .


Dla samej przyjemności , oto sposób na „zmuszenie” querySelectorAlldo uklęknięcia i ukłonu przed Tobą:

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

Teraz dobrze jest przejść przez tę funkcję, pokazując, kto jest szefem. Teraz nie wiem, co jest lepsze, tworząc zupełnie nową nazwaną otokę funkcji, a następnie cały kod używaj tej dziwnej nazwy (prawie w stylu jQuery) lub nadpisuj funkcję jak powyżej, aby reszta kodu nadal była w stanie aby użyć oryginalnej nazwy metody DOM querySelectorAll.

  • Takie podejście wyeliminowałoby ewentualne zastosowanie podmetod

Nie polecałbym tego w żaden sposób, chyba że szczerze nie dajesz [wiesz co].

vsync
źródło