可能是世界第三齐全的Object和Array的遍历(迭代)方法对比

106 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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_entries.png

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_values.png

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-infor-in(取值)Object.keys()Object.entries()Object.values()Object.getOwnPropertyNames()
包含原型链的属性
包含不可枚举的属性
是否可以终止遍历N/AN/AN/AN/A
遍历耗时(500万个属性)1013ms1227ms840ms5374ms2769ms2019ms
获取列表耗时(500万个属性)1258ms1346ms834ms4934ms2748ms2000ms

耗时

由于for-in和其他的方法的使用方法和产物上有差异,所以针对遍历获取列表两种情况进行了测试。

当数量在1000以下时,几个方法没有显著差异,都能在2ms以内完成处理。

当数量到达1w时,耗时的差异开始凸显,基本能在8ms内完成,最高和最低差距在2倍以内。

当数量到达10w时,最高和最低差距进一步拉开到3倍。

Array的比较

比较项forfor-offorEach()Object.keys()Object.entries()Object.values()Object.getOwnPropertyNames()
包含数组元素以外的属性(值)
包含空白元素
是否可以终止遍历N/AN/AN/AN/A
遍历耗时(1000万个属性)122ms211ms178msN/AN/AN/AN/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是不存在的。

forEach.png

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
  • 需要终止遍历的话,forfor-in二选一。
  • 后面4个在Array的绝大多数情况都用不上。