为什么document.querySelectorAll返回的是StaticNodeList而不是真正的数组?

148
我很烦恼,即使在Firefox 3.6中,我也不能只执行document.querySelectorAll(...).map(...),我仍然找不到答案,所以我想在Stack Overflow上发布这篇博客的问题: http://blowery.org/2008/08/29/yay-for-queryselectorall-boo-for-staticnodelist/ 有人知道为什么你没有得到一个数组吗?或者为什么StaticNodeList没有以一种可以使用mapconcat等方法继承自数组的方式呢?
(顺便说一句,如果你只想要一个函数,你可以做一些像NodeList.prototype.map = Array.prototype.map;的事情...但是为什么这个功能在第一次被故意屏蔽了呢?)

3
实际上,getElementsByTagName返回的并不是一个数组,而是一个集合。如果你想像使用数组一样使用它(例如使用concat方法等),你需要通过循环将该集合转换为一个数组,并将集合中的每个元素复制到数组中。这种情况从未有人抱怨过。 - Marco Demaio
7个回答

307

您可以使用 ES2015(ES6)扩展运算符

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

将 StaticNodeList 转换为项目数组。
以下是如何使用它的示例。

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


42
另一种方法是使用Array.from()Array.from(document.querySelectorAll('div')).map(x => console.log(x.innerHTML)) - Michael Berdyshev
3
工作得很好,但这个答案是使用map的一种奇怪的方式。 - quemeful
8
请注意,Array.from() 接受一个映射函数作为其第二个参数。这对于避免第二次迭代非常有用(它允许您在数组仍在构建时对其进行映射,因此只需一次迭代)。引用自 MDN:更明确地说,Array.from(obj, mapFn, thisArg) 的结果与 Array.from(obj).map(mapFn, thisArg) 相同,除了它不创建中间数组,并且mapFn 只接收两个参数(元素、索引),而不是整个数组,因为数组仍在构建中。 - Simone

96
我认为这是W3C的哲学决定。W3C DOM [spec]的设计与JavaScript的设计非常正交,因为DOM旨在成为平台和语言中立的。"getElementsByFoo()返回有序的NodeList"或"querySelectorAll()返回StaticNodeList"等决策非常故意,以便实现不必担心根据语言相关实现(如JavaScript和Ruby中的Arrays上可用的.map但C#中的Lists上*不*可用)对齐其返回的数据结构。
W3C的目标很低:他们会说一个NodeList应该包含一个只读的.length属性,类型为unsigned long,因为他们相信每个实现都至少可以支持*那个*,但他们不会明确地说[]索引运算符应该被重载以支持获取位置元素,因为他们不想阻碍一些想要实现getElementsByFoo()但无法支持运算符重载的小语言。它是规范中普遍存在的哲学。

John Resig曾经表达过与你类似的观点,他还补充道:

我的论点并不是说 NodeIterator不够像DOM,而是 它不够像JavaScript。它 没有充分利用JavaScript语言中的特性, 不能最大程度地发挥其能力...

我有些同情。如果DOM是专门针对JavaScript特性编写的,那么使用起来就会更加自然和直观。同时,我也理解W3C的设计决策。


谢谢,这有助于我理解情况。 - Kev
@Kev:我看到你在博客文章页面上的评论,询问如何将“StaticNodeList”转换为数组。我会支持@mck89的答案作为将“NodeList” /“StaticNodeList”转换为本地数组的方法,但这在IE(8明显)中会导致JScript错误,因为这些对象是托管/“特殊”的对象。 - Crescent Fresh
没错,这就是为什么我给他点了赞。但是有人取消了我的+1。你所说的hosted/special是什么意思? - Kev
1
@Kev:托管变量是由“主机”环境(例如Web浏览器)提供的任何变量。例如documentwindow等。IE通常会特别实现这些变量(例如作为COM对象),有时以微小而微妙的方式不符合正常用法,例如在给定StaticNodeListArray.prototype.slice.call可能会出错;) - Crescent Fresh

43

我不知道为什么它返回一个节点列表而不是一个数组,也许是因为像getElementsByTagName一样,当你更新DOM时它会更新结果。无论如何,将该结果转换为简单数组的非常简单的方法是:

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

然后你可以执行:

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

3
实际上,当您更新 DOM 时,它不会更新结果,因此是“静态”的。您需要手动再次调用 qSA 来更新结果。但 slice 行的加一是值得的。 - Kev
2
是的,就像Kev所说的那样:qSA结果集是静态的,getElementsByTagName()结果集是动态的。 - joonas.fi
IE8只支持在标准模式下使用querySelectorAll()函数。 - mbokil

13

除了Crescent提到的之外,如果你只想要一个函数,你可以像这样做: NodeList.prototype.map = Array.prototype.map

不要这样做!这完全不能保证工作。

没有JavaScript或DOM/BOM标准规定NodeList构造函数作为全局/ window属性存在,也没有规定由querySelectorAll返回的NodeList将继承它,亦没有其原型可写,也没有确保函数Array.prototype.map可以在NodeList上实际工作。

NodeList允许是'host对象'(在IE和一些旧版浏览器中是这样)。Array方法被定义为允许操作任何公开数字和长度属性的JavaScript 'native对象',但并不要求在host对象上工作(在IE中就不行)。

令人恼火的是,在DOM列表(所有列表,而不仅仅是StaticNodeList)上您无法获得所有数组方法,但没有可靠的方法解决这个问题。您将需要手动将每个DOM列表转换为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() {
    ...
});

我同意 +1。只是评论一下,我认为使用 "var array = []" 而不是 "var array = new Array(list.length)" 可以使代码更短。但如果您知道这样做可能会有问题,我很感兴趣。 - Marco Demaio
@MarcoDemaio:没有问题。new Array(n) 只是给 JS 引擎提供了数组最终长度的提示。这可能允许它预先分配那么多的空间,这将有可能在数组增长时避免一些内存重新分配,从而导致速度加快。虽然我不知道它是否在现代浏览器中实际有所帮助... 我会怀疑它没有显著的作用。 - bobince
3
现在它已经在Array.from()中实现了。 - Michael Berdyshev

12

1
虽然这可能并没有完全回答 OP 提出的“为什么”问题……但它仍然是一个有效、实用和简洁的解决方案,可以解决 OP 面临的问题,并且对我也很有效。许多其他帖子都谈到了覆盖 Element 原型的方法,这是危险的。我会选择使用这个解决方案。 - Eric Seastrand

3
我认为您可以简单地执行以下操作。
Array.prototype.map.call(document.querySelectorAll(...), function(...){...});

对我来说,它完美地发挥了作用。


-1

这是我想要添加到其他人提出的可能性范围内的一个选项。它仅供智力娱乐,不建议使用。


只是为了好玩,这里有一种方法可以“迫使”querySelectorAll向您屈服:

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

现在踩着那个函数感觉真不错,向它展示谁才是老大。

我不知道哪种方法更好,是创建一个全新的命名函数包装器然后让你的所有代码都使用那个奇怪的名字(基本上是jQuery风格),还是像上面一样覆盖该函数一次,这样你的其余代码仍然可以使用原始的DOM方法名称querySelectorAll

  • 这种方法将消除可能使用子方法的情况。

除非你真的无所谓,否则我绝不建议这样做。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接