先记住这个答案
普通对象要支持 for...of 和数组展开,需要提供可调用的 Symbol.iterator 方法,该方法返回带 next 方法的迭代器。next 每次返回对象,done 为 false 时 value 是下一项,done 为 true 表示结束。若希望对象可以重复遍历,应在每次调用 Symbol.iterator 时创建独立游标,而不是把唯一索引放在共享对象上。迭代器自身还可让 Symbol.iterator 返回自身,便于直接继续消费。持有外部资源时要另外考虑提前退出的 return 清理,协议本身不会替业务关闭任意资源。
- iterable 创建一次遍历,iterator 保存遍历进度
- next 必须返回符合协议的对象
- 重复遍历需要独立游标或明确单次消费契约
每次开始遍历都创建新的位置变量
下面把固定数据放在函数闭包里,并在 Symbol.iterator 内声明 index。两个迭代器共享只读数据来源,却有各自的位置,所以一个已经读到第二项不会改变另一个的起点。
返回的迭代器也实现 Symbol.iterator 并返回自身,表示对它继续遍历会接着当前进度走。外层对象则创建新实例,表示重新开始;把这两层区分开就能解释可重复与单次消费。
function makeLabels() {
const labels = ['draft', 'ready'];
return {
[Symbol.iterator]() {
let index = 0;
return {
next() {
if (index >= labels.length) return { done: true, value: undefined };
return { done: false, value: labels[index++] };
},
[Symbol.iterator]() { return this; }
};
}
};
}
const labels = makeLabels();
const first = labels[Symbol.iterator]();
const second = labels[Symbol.iterator]();
console.log(first.next().value, first.next().value, second.next().value);
console.log(JSON.stringify([...labels]));
console.log(JSON.stringify([...labels]));查看输出与解释
draft ready draft
["draft","ready"]
["draft","ready"]两个游标独立推进,两次展开外层对象都从头读取。若把 index 移到所有迭代器共享的位置,第二次遍历可能为空,这不是 for...of 缓存了错误结果,而是数据源契约改变了。
结束标志与业务值不能混为一谈
undefined 本身可以是合法元素,只要 done 为 false,消费者仍应把它作为这一项处理。不能使用 value 是否为空来判断结束,否则会丢失实际包含 undefined、零或空字符串的序列。
同样,next 返回的必须是对象,直接返回一个数字不符合协议。测试应覆盖空序列、单元素、重复调用结束后的 next 和交错使用两个游标,而不只确认一次展开看起来正常。
有资源的迭代器还要设计关闭路径
循环提前 break 时,协议可以调用迭代器的 return 方法进行收尾;内存固定序列没有外部资源,示例不需要额外关闭逻辑。若封装文件游标或其他资源,应明确结束和提前退出各自怎么释放。
同步迭代器不应把异步读取伪装成已经得到的 value。真正异步来源需要异步迭代协议和对应消费方式,同时定义取消与错误责任;给对象加一个方法名不能消除底层等待。
容易答错的地方
- 把所有遍历共享的 index 放在外层
- 如果外层对象宣称可重复遍历,这会让不同消费者互相推进位置,嵌套循环也可能得到意外结果。应每次创建独立游标;若本来只允许消费一次,则需要明确记录这个接口约定。
- 用 value 为 undefined 代表遍历结束
- 序列可以合法包含 undefined,结束由 done 表达。应检查完整结果对象并测试空值元素,避免把业务数据和协议控制信息混在同一条件里。
面试官还会怎么问?
只有 next 方法可以直接展开吗?
不一定,展开需要先取得 Symbol.iterator。可以让迭代器实现这个方法并返回自身,或者用一个外层可迭代对象返回它;next 与迭代入口分别满足不同层次的契约。
生成器会自动解决所有资源问题吗?
生成器简化状态保存,但仍需要在正确位置安排 finally 等清理逻辑,并由消费者正确结束迭代。若外部任务独立启动,结束生成器也不会自动取消所有业务副作用。
为什么不直接返回一个数组?
小型固定数据返回数组通常更简单。自定义迭代适合按需产生结果或提供特定读取顺序,但会增加状态与生命周期复杂度,应根据数据规模和接口需求选择,而非为了使用协议而封装。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。