先记住这个答案
数组 keys() 逐项产生数字索引,values() 产生对应值,entries() 产生 [索引, 值] 二元数组。它们返回可迭代的数组迭代器,可以用 next、for...of 或展开消费,并不会立即复制整个数组。同一个迭代器被读完后再次展开通常为空,需要重新调用方法获取新游标。对普通稀疏数组,keys 仍覆盖长度内的位置,values 与 entries 会读取缺失位置的值;这与只枚举自有可枚举字符串属性的 Object.keys 不同。若需要稳定快照,应明确复制时机。
- keys、values、entries 改变产出内容
- 返回的是有进度的迭代器而非结果数组
- Object.keys 与数组 keys 的枚举范围不同
通过两次展开观察游标消耗
示例先保存 entries 迭代器,再展开两次,第二次为空。重新调用 entries 才创建新游标;这解释了为什么调试日志先展开查看后,真正业务循环可能已经无数据可读。
keys 与 values 的结果也用展开显示,但这一步是消费者主动收集的,不能反过来说方法本身返回数组。需要数组方法时可以先物化,若只按顺序读取则不必额外分配完整副本。
const values = ['A', 'B'];
console.log(JSON.stringify([...values.keys()]));
console.log(JSON.stringify([...values.values()]));
const entries = values.entries();
console.log(JSON.stringify([...entries]));
console.log(JSON.stringify([...entries]));
console.log(JSON.stringify([...values.entries()]));查看输出与解释
[0,1]
["A","B"]
[[0,"A"],[1,"B"]]
[]
[[0,"A"],[1,"B"]]一个 entries 实例被第一次展开耗尽,第二次不再产生项;新调用返回的新迭代器从头开始。每个键值对里的索引是数字,不是 Object.keys 返回的字符串属性名。
稀疏数组让属性枚举差异更明显
new Array(3) 没有自有数字属性,但数组 keys 仍能产生零、一、二,因为它描述位置序列。Object.keys 则不会为这些缺失属性产生名称,二者长度可能完全不同。
若业务是遍历存在的记录,应确认是否允许稀疏输入,并选择检查自有属性还是处理所有位置。values 读取空位通常得到 undefined,原型上存在数字属性时还可能读到继承值,不能仅凭 length 推断真实记录。
迭代器不是创建时冻结的数据快照
尚未读取的值可能在消费期间被修改,数组长度变化也会影响尚未结束的迭代过程。若业务需要处理请求开始时那一批项目,应先建立受控快照,而不是保留一个游标就认为数据已经固定。
浅复制只能固定容器位置和当前元素引用,对象内部仍可能变化。批量处理若涉及并发更新,应定义快照层级、版本或稳定标识,避免把迭代顺序当作一致性协议。
容易答错的地方
- 对 entries 返回值直接使用普通数组 map
- 返回值是迭代器,不能假定具有目标环境里的数组方法。需要数组时显式收集,或者按迭代协议处理;即便新环境提供迭代器助手,也与 Array.prototype.map 的返回和惰性语义不同。
- 调试时展开后继续复用同一个迭代器
- 日志输出已经消费游标,后续循环可能为空。应记录数据源的独立快照或新建一个迭代器,不要在排查中改变本来要交给业务处理的消费状态。
面试官还会怎么问?
entries 的每一项是原数组里的二元组吗?
不是,数组 entries 把当前位置索引和读取值组成二元数组。若原值本身也是数组,它只是二元组的第二项,不会自动展开;应根据具体嵌套结构解构而不是凭名字猜层级。
values 与默认数组展开有什么关系?
普通数组的默认迭代行为就是依次产生值,所以展开数组与消费其 values 迭代器通常得到相同序列。自定义 Symbol.iterator 可能改变默认展开,而显式调用内建方法仍应按各自契约分析。
已结束的迭代器遇到后来追加的数据会恢复吗?
不应依赖这种恢复;数组迭代器完成后会保持结束状态。需要读取新一轮数据应重新创建游标,并定义新旧批次边界,避免一个长期复用的迭代器承担不明确的增量订阅职责。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。