使用 for 循环遍历 HTMLCollection 元素

666

我正在尝试获取 HTMLCollectionOf 中所有元素的 id。我编写了以下代码:

var list = document.getElementsByClassName("events");
console.log(list[0].id);
for (key in list) {
    console.log(key.id);
}

但是我在控制台中得到了以下输出:

event1
undefined

这不是我预期的结果。为什么第二个控制台输出是undefined,而第一个控制台输出是event1


14
注意事项:无论您选择哪种构造方式,请注意 getElementsByClassName 返回的是带有该 CSS 类的节点的实时集合。因此,如果您在循环中操作正在迭代的节点的 class 属性,则该集合可能会发生变化。在这种情况下,除了 Array.from(...)。forEach 之外,大多数构造方式都将失效。Array.from 将进行对象克隆并创建一个单独的对象,然后进行迭代。 - RBT
13个回答

1334

针对原问题,您在错误地使用 for/in。 在您的代码中,key 是索引。 因此,要从伪数组中获取值,您需要执行 list[key],要获取id,则需要执行list[key].id。 但是,首先就不应该使用for/in来做这件事。

摘要(2018年12月添加)

永远不要使用for/in来迭代 nodeList 或 HTMLCollection。 避免使用它的理由如下所述。

所有现代浏览器的最新版本(Safari、Firefox、Chrome、Edge)都支持对 DOM 列表(例如 nodeListHTMLCollection)进行for/of迭代。

以下是一个例子:

var list = document.getElementsByClassName("events");
for (let item of list) {
    console.log(item.id);
}

为了支持老旧浏览器(包括诸如 IE 等),可以使用以下代码,在所有浏览器中都能正常工作:

var list = document.getElementsByClassName("events");
for (var i = 0; i < list.length; i++) {
    console.log(list[i].id); //second console output
}

为什么不应该使用 for/in

for/in 用于迭代对象的属性。这意味着它将返回对象所有可迭代的属性。虽然它看起来可以用于数组(返回数组元素或伪数组元素),但它也可能返回对象的其他非预期的类似数组元素的属性。并且,注意了,一个 HTMLCollectionnodeList 对象都可以有其他属性,这些属性会在 for/in 迭代中被返回。我刚试过了,在 Chrome 中以你正在迭代的方式进行迭代,将检索列表中的项目(索引 0、1、2 等...),但也将检索到 lengthitem 属性。对于 HTMLCollection,for/in 迭代根本不起作用。


请参见 http://jsfiddle.net/jfriend00/FzZ2H/ 了解为什么不能使用 for/in 迭代 HTMLCollection。

在 Firefox 中,你的 for/in 迭代将返回这些项目(对象的所有可迭代属性):

0
1
2
item
namedItem
@@iterator
length

希望现在你能够理解为什么要使用 for (var i = 0; i < list.length; i++),这样你在迭代中就只会得到 012


NodeList 和 HTMLCollection 迭代的浏览器支持演变

以下是浏览器在 2015-2018 年间逐步演变的方式,为您提供了额外的迭代方法。现代浏览器已经无需使用这些方法,因为您可以使用上述选项。

2015 年 ES6 版本更新

ES6 中新增了 Array.from() 方法,可将类数组结构转换为实际数组。这使得可以像下面这样直接枚举列表:

"use strict";

Array.from(document.getElementsByClassName("events")).forEach(function(item) {
   console.log(item.id);
});

演示(截至2016年4月,适用于Firefox、Chrome和Edge):https://jsfiddle.net/jfriend00/8ar4xn2s/


ES6的更新(2016年)

现在您可以通过将以下内容添加到代码中,使用ES6 for/of结构来操作NodeListHTMLCollection

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
HTMLCollection.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];

然后,你可以这样做:

var list = document.getElementsByClassName("events");
for (var item of list) {
    console.log(item.id);
}

这在当前版本的Chrome、Firefox和Edge中可行。这是因为它将Array迭代器附加到NodeList和HTMLCollection原型上,因此当for/of对它们进行迭代时,它使用Array迭代器进行迭代。

工作演示:http://jsfiddle.net/jfriend00/joy06u4e/


2016年12月针对ES6的第二次更新

截至2016年12月,Chrome v54和Firefox v50已经内置了Symbol.iterator支持,因此下面的代码本身可以工作。但Edge还没有内置该功能。

var list = document.getElementsByClassName("events");
for (let item of list) {
    console.log(item.id);
}

工作演示(在Chrome和Firefox中):http://jsfiddle.net/jfriend00/3ddpz8sp/

2017年12月的第三次更新:ES6

截至2017年12月,在Edge 41.16299.15.0中,对于document.querySelectorAll()返回的nodeList可以正常使用ES6语法的for/of迭代器,但是对于document.getElementsByClassName()返回的HTMLCollection,需要手动分配迭代器才能在Edge中使用它。不明白为什么他们会修复一种集合类型而不修复另一种集合类型。但是,您现在可以在Edge的当前版本中使用ES6 for/of语法迭代document.querySelectorAll()的结果。

我还更新了上面的jsFiddle,以便单独测试HTMLCollectionnodeList,并将输出捕获在jsFiddle本身中。

2018年3月的第四次更新:ES6

根据mesqueeeb的说法,Safari也内置了对Symbol.iterator的支持,因此您可以使用for(let item of list)迭代器来迭代document.getElementsByClassName()document.querySelectorAll()

2018年4月的第五次更新:ES6

显然,Edge 18将于2018年秋季支持使用for/of迭代器迭代HTMLCollection

2018年11月的第六次更新:ES6

我可以确认,在包含在2018年秋季Windows更新中的Microsoft Edge v18中,现在可以使用for/of迭代器迭代HTMLCollectionnodeList

因此,现代浏览器都原生支持对HTMLCollection和NodeList对象使用for/of迭代器。


2
感谢您一直以来所做的出色更新。顺便问一下,您知道他们是否会将Symbol.iterator添加到HTMLCollection规范中吗? 我知道所有浏览器都在这样做,但据我所知,规范中没有提到它,而TypeScript定义是基于规范而非实现构建的。 - WORMSS
1
@WORMSS - 我不确定。但是,如果我要猜的话,在LivingStandard文件中的这个注释: HTMLCollection是一个历史文物,我们无法摆脱。虽然开发人员当然可以继续使用它,但新的API标准设计者不应该使用它(在IDL中使用sequence<T>代替)听起来它不太可能再得到改进了,因为他们不想再鼓励你使用实时集合API-很容易在修改DOM的同时迭代实时集合而导致错误。 - jfriend00
为了在TypeScript中使用“for (let item of list)”语法,我需要在tsconfig.json文件中添加“dom.iterable”,请参考https://dev59.com/9FMH5IYBdhLWcg3w6VUv#60342425。 - xhafan

95

你无法在NodeListHTMLCollection上使用for/in。但是,你可以使用一些Array.prototype方法,只要你使用.call()NodeListHTMLCollection作为this传递给它们。

因此,考虑以下替代jfriend00的for循环

var list= document.getElementsByClassName("events");
[].forEach.call(list, function(el) {
    console.log(el.id);
});

这个技巧在MDN上有一篇很好的文章进行了讲解。请注意他们关于浏览器兼容性的警告:

[...]在本机方法(如forEach)中将宿主对象(如NodeList)作为this传递是不能保证在所有浏览器中运行的,并且已知在某些浏览器中会失败。

因此,虽然这种方法很方便,但使用for循环可能是最兼容的解决方案。

更新(2014年8月30日):最终您将能够使用ES6 for /of

var list = document.getElementsByClassName("events");
for (const el of list)
  console.log(el.id);

最近版本的Chrome和Firefox已经支持了。


1
非常好!我使用了这种技术来获取<select multiple>中所选选项的值。例如:[].map.call(multiSelect.selectedOptions, function(option) { return option.value; }) - XåpplI'-I0llwlg'I -
1
我正在寻找一个ES2015的解决方案,所以感谢您确认for ... of可以工作。 - Richard Turner

87

在ES6中,您可以执行类似于[...collection]Array.from(collection)的操作。

let someCollection = document.querySelectorAll(someSelector)
[...someCollection].forEach(someFn) 
//or
Array.from(collection).forEach(someFn)

例:

    navDoms = document.getElementsByClassName('nav-container');
    Array.from(navDoms).forEach(function(navDom){
     //implement function operations
    });

@DanielM,你猜我做了什么?我浅克隆了一个类似数组的结构。 - mido
我明白了,谢谢——现在我找到了我要找的文档:https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Spread_operator - DanielM
1
我总是使用这个,比Array.from更容易阅读,但我想知道它是否有明显的性能或内存缺陷。例如,如果我需要迭代表格行的单元格,我会使用 [...row.cells].forEach 而不是 row.querySelectorAll('td') - Mojimi

17

你可以添加这两行代码:

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

getElementsByClassNamegetElementsByTagName返回的是HTMLCollection

querySelectorAll返回的是NodeList

这样,您可以使用forEach

var selections = document.getElementsByClassName('myClass');

/* alternative :
var selections = document.querySelectorAll('.myClass');
*/

selections.forEach(function(element, i){
//do your stuffs
});

这个答案似乎非常有效。问题在哪里? - Peheje
3
问题在于这个解决方案在IE11上不起作用!但是,这仍然是一个很好的解决方案。 - Rahul Gaba
6
请注意,NodeList已经拥有forEach()方法 - Franklin Yu
我对浏览器对此的支持很好奇。看起来很棒。 - Muhammed

12

使用 Array.prototype.forEach.call 是替代 Array.from 的一种方法。

forEach:Array.prototype.forEach.call(htmlCollection, i => { console.log(i) });

map:Array.prototype.map.call(htmlCollection, i => { console.log(i) });


10

如果您使用的是IE9或更高版本,则没有理由使用es6功能来避免for循环。

在ES5中,有两个好的选择。首先,您可以像evan mentions一样“借用”ArrayforEach

但更好的选择是...

使用Object.keys(),它确实具有forEach和自动过滤“自有属性”的功能。

也就是说,Object.keys基本上相当于使用HasOwnProperty进行for... in,但更加平稳。

var eventNodes = document.getElementsByClassName("events");
Object.keys(eventNodes).forEach(function (key) {
    console.log(eventNodes[key].id);
});

7

我在IE 11和Firefox 49中使用forEach遇到了问题,但是我找到了以下解决方法:

Array.prototype.slice.call(document.getElementsByClassName("events")).forEach(function (key) {
        console.log(key.id);
    }

IE11的绝佳解决方案!曾经是一种常见技术... - mb21

5

截止至2016年3月,Chrome 49.0支持使用for...of循环遍历HTMLCollection

this.headers = this.getElementsByTagName("header");

for (var header of this.headers) {
    console.log(header); 
}

请看此处的文档。但是要在使用for...of之前应用以下解决方法才能正常工作:
HTMLCollection.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];

如果要使用for...of遍历NodeList,同样需要这样做:

NamedNodeMap.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];

我相信/希望for...of很快就能够不需要上面的解决方法来工作。相关的问题在这里:

https://bugs.chromium.org/p/chromium/issues/detail?id=401699

更新:请看Expenzor的评论:自2016年4月起,此问题已得到修复。您不需要添加HTMLCollection.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];来使用for...of迭代HTMLCollection。


4
截至2016年4月,这个问题已得到解决。您不需要添加 HTMLCollection.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; 以使用 for...of 迭代 HTMLCollection - Expenzor

4

我经常使用的简单解决方法

let list = document.getElementsByClassName("events");
let listArr = Array.from(list)

接下来,您可以在所选内容上运行任何所需的数组方法。

listArr.map(item => console.log(item.id))
listArr.forEach(item => console.log(item.id))
listArr.reverse()

3

在边缘

if(!NodeList.prototype.forEach) {
  NodeList.prototype.forEach = function(fn, scope) {
    for(var i = 0, len = this.length; i < len; ++i) {
      fn.call(scope, this[i], i, this);
    }
  }
}

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