Почему document.querySelectorAll возвращает StaticNodeList, а не реальный массив?

Мне кажется, что я не могу просто сделать document.querySelectorAll(...).map(...) даже в Firefox 3.6, и я до сих пор не могу найти ответ, поэтому я думал, что перейду на SO вопрос из этого блога:

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

Кто-нибудь знает о технической причине, почему вы не получаете Array? Или почему StaticNodeList не наследует от массива таким образом, что вы можете использовать map, concat и т.д.

(BTW, если это только одна функция, которую вы хотите, вы можете сделать что-то вроде NodeList.prototype.map = Array.prototype.map;... но опять же, почему эта функция (намеренно?) заблокирована в первую очередь?)

Ответ 1

Я считаю, что это философское решение W3C. Дизайн W3C DOM [spec] довольно ортогонален дизайну JavaScript, так как DOM предназначен для нейтральной платформы и языка.

Решения типа "getElementsByFoo() возвращает упорядоченный NodeList" или "querySelectorAll() возвращает a StaticNodeList", очень интенсивные, так что реализациям не нужно беспокоиться о выравнивании возвращаемой структуры данных на основе языка зависимые реализации (например, .map доступны в массивах в JavaScript и Ruby, но не в списках на С#).

Цель W3C низкая: они скажут, что NodeList должно содержать свойство readonly .length типа unsigned long, потому что они верят каждая реализация может хотя бы поддержать это, но они не будут говорить явно, что оператор индекса [] должен быть перегружен, чтобы поддерживать получение позиционных элементов, потому что они не хотят замалчивать какой-то плохой язык, который приходит, который хочет реализовать getElementsByFoo(), но не может поддерживать перегрузку оператора. Это распространенная философия присутствует на протяжении большей части спецификации.

John Resig озвучил аналогичный вариант как ваш, к которому он добавляет:

Мой аргумент не настолько, что NodeIterator не очень похож на DOM что он не очень похож на JavaScript. Это не использует возможности присутствовать на языке JavaScript и использовать их в меру своих возможностей...

Я немного сочувствую. Если DOM был написан специально с функциями JavaScript в виду, это было бы намного менее неудобно и более интуитивно понятным в использовании. В то же время я понимаю решения W3C.

Ответ 2

Вы можете использовать ES2015 (ES6) оператор распространения:

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

преобразует StaticNodeList в массив элементов.

Вот пример того, как его использовать.

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

Ответ 3

Я не знаю, почему он возвращает список node вместо массива, возможно, потому что, как getElementsByTagName, он обновит результат при обновлении DOM. В любом случае очень простой способ преобразования этого результата в простой массив:

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

а затем вы можете:

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

Ответ 4

Просто чтобы добавить к тому, что сказал Полумесяц,

Если это только одна функция, которую вы хотите, вы можете сделать что-то вроде NodeList.prototype.map = Array.prototype.map

Не делайте этого! Это вовсе не гарантирует работу.

Никакой стандарт JavaScript или DOM/BOM не указывает, что конструктор-функция NodeList существует даже как глобальное /window свойство, или что NodeList, возвращенный querySelectorAll, наследует от него или что его прототип доступен для записи или что функция Array.prototype.map будет фактически работать с NodeList.

NodeList разрешено быть "объектом-хозяином (и является одним, в IE и некоторых старых браузерах). Методы Array определяются как позволяющие работать с каким-либо собственным объектом JavaScript, который предоставляет свойства numeric и length, но они не обязаны работать с объектами хоста (а в IE они этого не делают).

Досадно, что вы не получаете все методы массива в списках DOM (все они, а не только StaticNodeList), но нет надежного способа обойти его. Вам нужно будет преобразовать каждый список DOM, который вы возвращаете в массив вручную:

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

Ответ 5

Я думаю, вы можете просто сделать следующее

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

Он отлично работает для меня

Ответ 6

Это вариант, который я хотел бы добавить к ряду других возможностей, предлагаемых здесь другими. Он предназначен только для интеллектуального веселья и не рекомендуется.


Просто для удовольствия, вот способ "заставить" querySelectorAll встать на колени и поклониться вам:

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

Теперь приятно перешагнуть через эту функцию, показывая, кто в ней хозяин. Теперь я не знаю, что лучше: создать совершенно новую именованную функцию-обертку, а затем заставить весь ваш код использовать это странное имя (в значительной степени в стиле jQuery) или переопределить функцию, как описано выше, чтобы остальная часть вашего кода все еще могла использовать оригинальное имя метода DOM querySelectorAll.

  • Такой подход исключит возможное использование под-методов

Я не рекомендовал бы это ни в коем случае, если вы честно не даете [вы знаете, что].

Ответ 7

Я думаю, что это более компактный подход, предложенный Michael Berdyshev выше в комментариях.

Array.from(document.querySelectorAll("*"))