为什么NodeList没有forEach方法?

109

我正在编写一个简短的脚本来更改<abbr>元素的内部文本,但发现nodelist没有forEach方法。我知道nodelist没有继承自Array,但是forEach似乎是一个有用的方法,难道不能将forEach添加到nodelist中吗?是否有我不知道的特定实现问题阻止了将forEach添加到nodelist中?

注意:我知道Dojo和jQuery都以某种形式为它们的节点列表提供forEach。由于存在限制,我无法使用任何一个。


8
来自未来的问候!ES6 中新增了 NodeList 的 forEach 方法 - Blaise
@Blaise 自 ECMAScript 5 以来,Array.prototype.forEach 就已经存在了,事实上,NodeList.prototype.forEach === Array.prototype.forEach。但是这个事实并没有在 ECMAScript 中被 _指定_,而是在 WebIDL 中被指定的。 - Sebastian Simon
在按类选择时使用 .forEach()getElementsByClassName() 返回一个没有 .forEach() 的 HTMLCollection,可以使用返回 nodeList 的 querySelectorAll('.classname')。 - DWB
11个回答

111

所有主流浏览器现在都支持NodeList中的forEach()

请参见MDN上的nodeList forEach()

原始答案

这些答案都没有解释为什么NodeList不继承自Array,因此它可以拥有forEach和其他所有功能。

答案可以在这个es-discuss线程中找到。简而言之,它会破坏网络:

问题在于错误地假设instanceof与Array.prototype.concat结合使用时实例是一个数组。

Google的Closure库中存在一个错误,导致几乎所有Google应用程序都因此失败。一旦发现这个问题,就会更新该库,但可能仍然存在某些代码与concat结合使用时做出相同的错误假设。

也就是说,一些代码执行了类似以下操作的内容:

if (x instanceof Array) {
  otherArray.concat(x);
} else {
  doSomethingElseWith(x);
}

然而,concat将与其他对象不同地处理“真实”数组(不是 instanceof Array):

[1, 2, 3].concat([4, 5, 6]) // [1, 2, 3, 4, 5, 6]
[1, 2, 3].concat(4) // [1, 2, 3, 4]

那么这意味着当 x 是 NodeList 时,上面的代码就会出错,因为在它走过 doSomethingElseWith(x) 这条路径之前,而后来又走了 otherArray.concat(x) 这条路径,由于 x 不是真正的数组,所以会出现一些奇怪的问题。
有一段时间提出了一个名为 Elements 的类,它是 Array 的一个真正的子类,并且将被用作“新的NodeList”。然而,由于技术和规范相关的原因,它目前已经从 DOM 标准中被移除,至少暂时无法实现。

14
对我来说似乎是个糟糕的决定。但说真的,我认为有时候故意破坏一些东西是正确的决定,特别是如果这意味着我们未来会有合理的API。此外,网络平台甚至远没有稳定下来,人们已经习惯了他们2年前的JavaScript不再按预期运行。 - JoyalToTheWorld
8
“Breaks the web”不等于“破坏谷歌的东西”。 - Matt
为什么不直接从Array.prototype中借用forEach方法呢?例如,不要添加Array作为原型,只需在构造函数中执行this.forEach = Array.prototype.forEach,或者甚至为NodeList独特地实现forEach方法? - PopKernel
2
注意:此次更新“NodeList现在在所有主流浏览器中都具有forEach()”似乎意味着IE不是一个主要的浏览器。希望对某些人来说是真的,但对我来说(尚)不是真的。 - Graham P Heath
奇怪的是,截至2021年11月,它仍未包含在whatwg规范中。 - Jacob C.
可迭代的<Node>声明中隐含了这一点。 - Domenic

67

你可以做到

Array.prototype.forEach.call (nodeList, function (node) {

    // Your code here.

} );

4
可以将Array.prototype.forEach.call缩写为[].forEach.call - CodeBrauer
8
@CodeBrauer正在做的不仅仅是缩短 Array.prototype.forEach.call 的写法,他还创建了一个空数组并使用其 forEach 方法。 - Paul D. Waite
据我所知,Array.prototype.forEach.call 是最快的方法,同时不会分配额外的内存。 - Kurt Riede

33

你可以考虑创建一个新的节点数组。

  var nodeList = document.getElementsByTagName('div'),

      nodes = Array.prototype.slice.call(nodeList,0); 

  // nodes is an array now.
  nodes.forEach(function(node){ 

       // do your stuff here.  

  });
注意:这里只是创建一个节点引用的列表/数组,没有重复的节点。
  nodes[0] === nodeList[0] // will be true

22
或者只需使用Array.prototype.forEach.call(nodeList, fun) - akuhn
我还建议给forEach函数取一个别名,这样可以这样写: var forEach = Array.prototype.forEach.call(nodeList, callback);。现在你只需要调用 forEach(nodeList, callback); 即可。 - Andrew Craswell

20

永远不要说永远,现在是2016年,在最新的Chrome浏览器(v52.0.2743.116)中,NodeList对象已经实现了一个forEach方法。

尽管其他浏览器还不支持它(测试FF 49),但现在使用它还为时过早,但我猜这很快就会被标准化。


2
Opera 也支持它,Firefox 的 v50 版本也将支持它,计划于 2016 年 11 月 15 日发布。 - Shaggy
1
尽管已经实现,但它不属于任何标准。最好还是使用Array.prototype.slice.call(nodelist).forEach(…),这是标准的,在旧浏览器中也能运行。 - Nate

20

简而言之,这是一个设计冲突,无法在NodeList上实现该方法。

来自MDN:

为什么我不能在NodeList上使用forEach或map?

NodeList非常像数组,并且使用Array.prototype方法对它们进行操作很有诱惑力。然而,这是不可能的。

JavaScript有一种基于原型的继承机制。数组实例继承了数组方法(如forEach或map),因为它们的原型链看起来像下面这样:

myArray --> Array.prototype --> Object.prototype --> null(对象的原型链可以通过多次调用Object.getPrototypeOf获得)

forEach、map和类似方法是Array.prototype对象的自有属性。

与数组不同,NodeList的原型链看起来像下面这样:

myNodeList --> NodeList.prototype --> Object.prototype --> null

NodeList.prototype包含item方法,但没有Array.prototype中的方法,因此它们无法在NodeLists上使用。

来源:https://developer.mozilla.org/zh-CN/docs/Web/API/NodeList (滚动到为什么我不能在NodeList上使用forEach或map?


8
既然这是一个列表,为什么要这样设计呢?他们为什么不把链条设计成:myNodeList --> NodeList.prototype --> Array.prototype --> Object.prototype --> null? - Igor Pantović

15

如果你想在NodeList上使用forEach,只需从Array中复制该函数即可:

NodeList.prototype.forEach = Array.prototype.forEach;

好了,现在您可以像对待数组一样使用它:

document.querySelectorAll('td').forEach(function(o){
   o.innerHTML = 'text';
});

4
除了修改基类不太容易被普通读者理解外,没有其他问题。换句话说,在某些代码深处,您必须记住对浏览器基本对象所做的每个自定义设置。依赖MDN文档已不再有用,因为对象的行为与常规不同。最好在调用时明确应用原型,这样读者可以轻松地意识到forEach是借用的思想,而不是语言定义的一部分。请参见@akuhn上面的答案。 - Sukima
@Sukima 由于错误的前提而误导了。 在这种特定情况下,给定的方法修复了NodeList以按开发人员所期望的方式运行。这是修复系统类问题的最适当方式。(NodeList似乎未完成,应在将来的语言版本中修复。) - Daniel Garmoshka
1
现在有了NodeList.forEach,这个polyfill就变得非常简单幽默了! - Damon

13
在ES2015中,您现在可以使用forEach方法来遍历nodeList。
document.querySelectorAll('abbr').forEach( el => console.log(el));

请查看MDN链接

但是如果您想要使用HTML集合或其他类似数组的对象,在es2015中,您可以使用Array.from()方法。该方法接受一个类数组或可迭代对象(包括节点列表、HTML集合、字符串等),并返回一个新的数组实例。您可以像这样使用它:

const elements = document.getElementsByTagName('abbr');
Array.from(elements).forEach( el => console.log(el));

由于Array.from()方法可以进行模拟,您可以在ES5代码中像这样使用它。

var elements = document.getElementsByTagName('abbr');
Array.from(elements).forEach( function(el) {
    console.log(el);
});

详见MDN页面。

检查当前浏览器支持情况

OR

另一种使用es2015的方法是使用展开运算符。

[...document.querySelectorAll('abbr')].forEach( el => console.log(el));

MDN 展开语法

展开运算符 - 浏览器支持性


1
我的解决方案:

//foreach for nodeList
NodeList.prototype.forEach = Array.prototype.forEach;
//foreach for HTML collection(getElementsByClassName etc.)
HTMLCollection.prototype.forEach = Array.prototype.forEach;

2
在DOM中通过原型扩展功能通常不是一个好主意,特别是在旧版本的IE中(文章)。 - KFE

1

0
请查看 MDN NodeList.forEach 规范。
NodeList.forEach(function(item, index, nodeList) {
    // code block here
});

在IE中,使用{{link1:akuhn的答案}}:
[].forEach.call(NodeList, function(item, index, array) {
    // code block here
});

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