先记住这个答案
keys()、values()、entries() 返回的都是数组迭代器,分别逐个产出下标、值和 [下标, 值] 二元数组。迭代器不是数组,可以用 for...of、展开语法或者手动调用 next() 来读取,但它没有 length,也没有 map、filter。它内部有一个游标,每读一项往前走一步,走到末尾后就一直是结束状态,所以同一个迭代器第二次展开得到的是空数组。要重新遍历,就再调用一次方法拿一个新的迭代器。
- 返回的是迭代器,不是数组,没有
map、length - 迭代器读完就空了,重新遍历要再调用一次
- 迭代器是惰性的,遍历时才去读数组的当前内容
arr.keys()包含空位下标,Object.keys不包含
返回值和「只能用一次」
先把三个方法的产出和重复消费的结果打印出来。
const arr = ['a', 'b']
console.log(JSON.stringify([...arr.keys()]))
console.log(JSON.stringify([...arr.values()]))
console.log(JSON.stringify([...arr.entries()]))
const it = arr.entries()
console.log(Array.isArray(it), typeof it.next) // 不是数组,但有 next 方法
console.log([...it].length, [...it].length) // 第二次已经读完了查看输出与解释
[0,1]
["a","b"]
[[0,"a"],[1,"b"]]
false function
2 0迭代器可以想成书签。第一次展开从头读到尾,书签停在最后一页;第二次展开从书签的位置接着读,后面已经没有内容了。每次调用 arr.entries() 都会给你一个新书签,放在第一页。
这个特性在把迭代器存进变量后最容易踩坑。比如先用 [...it] 打日志调试,后面的业务逻辑再用 it 就什么都拿不到了。
迭代器是惰性的
迭代器创建时不会复制数组,它只记住「当前数组」和「读到第几个了」。每次调用 next(),才去数组上读当前下标的值,并和当时的 length 比较决定是否结束。
const list = [1, 2]
const it = list.values()
list.push(3) // 迭代器创建之后才加的元素
console.log(JSON.stringify([...it]))
const sparse = ['a', , 'c'] // 下标 1 是空位
console.log(JSON.stringify([...sparse.keys()]), JSON.stringify(Object.keys(sparse)))查看输出与解释
[1,2,3]
[0,1,2] ["0","2"]第一段里 3 是在迭代器创建后才 push 的,但遍历时照样读到了,这就是惰性的表现。需要一份固定的快照,就在创建迭代器之前先复制数组。
第二段是和 Object.keys 的区别。arr.keys() 按 length 从 0 数到末尾,空位的下标也算;Object.keys 只列出真实存在的属性,空位没有属性,所以被跳过了。另外 Object.keys 返回的是字符串数组,arr.keys() 产出的是数字。
- 最常用的写法:同时拿下标和值
for (const [i, item] of arr.entries())是最常见的用法。和forEach((item, i) => ...)相比,它可以用break提前退出,也可以在循环里await。
容易答错的地方
- 以为
entries()返回的是二维数组 - 它返回的是迭代器,直接调用
arr.entries().map(...)会报TypeError。需要数组时先Array.from(arr.entries())或者展开。 - 以为
arr.keys()和Object.keys(arr)一样 - 差别有三个:前者是迭代器,后者是数组;前者产出数字,后者是字符串;前者包含空位下标,后者只包含实际存在的属性,还会包含你手动加在数组上的非数字属性。
面试官还会怎么问?
for...of 遍历数组时用的是哪个迭代器?
用的是 values(),因为 Array.prototype[Symbol.iterator] 和 Array.prototype.values 是同一个函数,所以 for (const x of arr) 和 for (const x of arr.values()) 效果一样。
Map 和 Set 也有这三个方法吗,有什么不同?
都有,返回的也是迭代器。Map 的 entries() 产出 [键, 值],是它的默认迭代器;Set 没有键的概念,keys() 和 values() 都产出元素本身,entries() 产出 [元素, 元素],这样设计是为了和 Map 的接口保持一致。
迭代器读完后能不能手动重置?
不能。数组迭代器没有提供重置的方法,读完后 next() 永远返回 { value: undefined, done: true }。需要反复使用就保存数组本身,每次用时重新调用 values(),或者一开始就用 Array.from 转成数组。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。