持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情
在上一篇文章 for-in vs. for-of 中对比了两种字面上相似的语法的差异,之后觉得有必要再整理一下横向对比Javascript中对象(数组)的遍历/迭代的各种方法。
常见的遍历/迭代方法
for
最基础最常规的遍历语法,几乎大部分编程语言必备的语法。任何需要循环执行(不限于遍历)的代码都可以通过for循环实现。
for-in
ES5加入的语法,遍历获取对象的属性名(key),具体查看 for-in vs. for-of
for-of
ES6加入的语法,遍历获取可迭代对象的值(value),具体查看 for-in vs. for-of
Object.keys()
Object的静态方法,返回一个包含该对象所有可枚举的属性名(key)的数组。
var obj = {a: 123, b: 'zzz', c: true}
Object.keys(obj).forEach(k => {
console.log(k)
})
// a
// b
// c
Object.entries()
Object的静态方法,是在ES2018新加入,返回一个包含该对象所有可枚举的属性的名和值(每个属性的名和值以数组形式组成:[key, value])的数组。
var obj = {a: 123, b: 'zzz', c: true}
Object.entries(obj).forEach(k => {
console.log(k)
})
// ['a', 123]
// ['b', 'zzz']
// ['c', true]
兼容性:
Object.values()
Object的静态方法,是在ES2018新加入,返回一个包含该对象所有可枚举的属性值(value)的数组。
var obj = {a: 123, b: 'zzz', c: true}
Object.values(obj).forEach(k => {
console.log(k)
})
// 123
// zzz
// true
兼容性:
Object.getOwnPropertyNames()
Object的静态方法,是在ES2018新加入,返回一个包含该对象所有可枚举的属性值(value)的数组。
var obj = {a: 123, b: 'zzz', c: true}
Object.values(obj).forEach(k => {
console.log(k)
})
// 123
// zzz
// true
兼容性:
Array.forEach()
Array的实例方法,遍历获取数组的每一个元素。
var arr = [123, 'zzz', true]
arr.forEach(k => {
console.log(k)
})
// 123
// zzz
// true
由于Object和Array之间存在差异,我将会分别针对这两种数据对象进行比较。
Object的比较
| 比较项 | for-in | for-in(取值) | Object.keys() | Object.entries() | Object.values() | Object.getOwnPropertyNames() |
|---|---|---|---|---|---|---|
| 包含原型链的属性 | 是 | 是 | 否 | 否 | 否 | 否 |
| 包含不可枚举的属性 | 否 | 否 | 否 | 否 | 否 | 是 |
| 是否可以终止遍历 | 是 | 是 | N/A | N/A | N/A | N/A |
| 遍历耗时(500万个属性) | 1013ms | 1227ms | 840ms | 5374ms | 2769ms | 2019ms |
| 获取列表耗时(500万个属性) | 1258ms | 1346ms | 834ms | 4934ms | 2748ms | 2000ms |
耗时
由于for-in和其他的方法的使用方法和产物上有差异,所以针对遍历和获取列表两种情况进行了测试。
当数量在1000以下时,几个方法没有显著差异,都能在2ms以内完成处理。
当数量到达1w时,耗时的差异开始凸显,基本能在8ms内完成,最高和最低差距在2倍以内。
当数量到达10w时,最高和最低差距进一步拉开到3倍。
Array的比较
| 比较项 | for | for-of | forEach() | Object.keys() | Object.entries() | Object.values() | Object.getOwnPropertyNames() |
|---|---|---|---|---|---|---|---|
| 包含数组元素以外的属性(值) | 否 | 否 | 否 | 是 | 是 | 是 | 是 |
| 包含空白元素 | 是 | 是 | 否 | 否 | 否 | 否 | 否 |
| 是否可以终止遍历 | 是 | 是 | 否 | N/A | N/A | N/A | N/A |
| 遍历耗时(1000万个属性) | 122ms | 211ms | 178ms | N/A | N/A | N/A | N/A |
数组元素以外的属性 以及 空白元素
for-of的执行是依赖于迭代器,在获取迭代器的方法GetIterator()中调用对象原型链上的[@@iterator]()方法,而Array的这个方法即Array.prototype.values()。根据ECMA官网 Array Iterator Objects的CreateArrayIterator 所示,Array的iterator是根据索引从0到length-1去构造出来的,而并没有判断该索引是否存在和有值,所以不包括数组元素以外的属性,且包括空白元素。
forEach在遍历的过程中,只返回非空白元素不包含数组元素以外的属性,例如:
const arr = [111, 222, , 'abc', 333, , undefined, 'hhhhh', null, 444]
arr.foo = 'dddd'
arr.__proto__.ver = '0.1'
const ls = []
arr.forEach(item => {
ls.push(item)
})
console.log(ls)
// [111, 222, 'abc', 333, undefined, 'hhhhh', null, 444]
究其原因,是因为forEach方法背后执行的逻辑,首先根据索引从0到length-1去调用HasProperty()进行检查,如果这个索引不存在,则跳到下一个继续判断。而上面代码中的arr,通过console中可以佐证,索引2、5是不存在的。
Object.keys()、Object.entries()、Object.values()和Object.getOwnPropertyNames(),则是会调用一个Object对象的内部方法[[OwnPropertyKeys]],这个内部方法获取指定对象的属性名列表,同样类似上图所示的原因,所以会获得元素以外的属性,同时空白元素也是会被过滤。
终止遍历
Array.forEach()不允许终止遍历,会触发报错。
Uncaught SyntaxError: Illegal break statement
耗时
由于数组只进行遍历耗时的对比,而Object.keys()、Object.entries()、Object.values()和Object.getOwnPropertyNames()的输出都是原来的数组内容或者索引,所以就不参与比较。
在10w条记录以下的话,耗时的差异不显著。
100w条记录开始有明显差异,后两者的耗时比前者多30%以上,for-of的耗时比forEach()还要再多10%左右。
1000w条记录时,最快的for和最慢的for-of,差距已经达到70~80%。
结论
Object的话:
- 有条件跳出遍历选
for-in,全部遍历或者获取列表(key/value)选Object.keys() - 需要包括原型链属性选
for-in - 需要包括不可枚举的属性选
Object.getOwnPropertyNames()
Array的话:
- 10w条数据以下的情况,前三种请随意。10w以上的话,无特别要求建议优先
for - 需要终止遍历的话,
for和for-in二选一。 - 后面4个在Array的绝大多数情况都用不上。