在JavaScript里,迭代器是一种按顺序访问数据集合的机制。它并不是一个具体的类,而是一套约定:只要一个对象实现了next方法,并且这个方法每次调用返回一个包含value和done属性的结果对象,那这个对象就可以被称为迭代器。与之相关的还有可迭代协议,指的是对象自身或原型链上部署了Symbol.iterator方法,调用该方法会返回一个迭代器。语言内建的数组、字符串、Map、Set都遵循这套协议,所以能直接使用for...of语法。

一、迭代器与可迭代协议的基础原理
迭代器的核心在于把“如何取下一个元素”的逻辑封装起来。一个最简的迭代器可以写成普通对象,它的next方法内部维护一个指针,每次调用移动指针并返回当前值。done为false时表示还有数据,为true时表示遍历结束。这样的设计让使用者不必关心数据结构是数组、链表还是自定义对象,只要拿到迭代器就能统一处理。
可迭代协议则是更高一层的约定。如果一个对象可以被for...of循环,那它必须是可迭代的。判定标准非常简单:对象或者它的原型上存在一个键为Symbol.iterator的方法,且该方法执行后返回一个符合迭代器协议的对象。Symbol.iterator是ES6引入的知名符号,用来避免和普通属性名冲突。下面代码展示了一个手写的数字范围迭代器:
function createRangeIterator(start, end) {
let current = start;
return {
next: function() {
if (current <= end) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
const it = createRangeIterator(1, 3);
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }
上面的createRangeIterator返回的对象只实现了next,所以它是迭代器,但还不能直接用于for...of。要让它可迭代,需要再包一层,让对象拥有Symbol.iterator方法并返回自身或另一个迭代器。
二、为什么普通对象默认不可迭代
日常写代码时,我们习惯用for...of遍历数组,但同样的方式用在普通对象上会抛出TypeError,提示对象不是可迭代的。原因在于普通对象的原型链上没有Symbol.iterator。JavaScript设计者故意这样安排,因为对象的键是无序字符串或Symbol,直接迭代容易让开发者误以为有固定顺序,而数组和Map才强调序列。
不过这不代表对象不能迭代。很多时候我们希望遍历对象的某个内部集合,比如把所有属性值依次取出,或者把键值对当成数列处理。此时就需要手动让对象满足可迭代协议。注意,这里说的是对象“变得可迭代”,不是改变语言规则,而是在对象上显式添加Symbol.iterator方法。
三、使用Symbol.iterator让对象可迭代
最直接的做法是给目标对象定义一个Symbol.iterator属性,其值是一个函数,函数返回一个迭代器。最简单的返回方式是用生成器函数,因为生成器本身就实现了迭代器协议。下面例子让一个保存用户列表的对象支持for...of,依次产出每个用户名称:
const userGroup = {
users: ['Alice', 'Bob', 'Charlie'],
[Symbol.iterator]: function() {
let index = 0;
const self = this;
return {
next: function() {
if (index < self.users.length) {
return { value: self.users[index++], done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const name of userGroup) {
console.log(name);
}
// 输出 Alice Bob Charlie
这段代码中,userGroup通过计算属性名[Symbol.iterator]挂上了迭代方法。for...of在启动时调用这个方法拿到迭代器,然后反复调用next直到done为true。由于逻辑写在next里,外部完全不需要知道users数组的存在,这就是封装带来的好处。
如果觉得手写next太繁琐,可以利用生成器大幅简化。生成器函数内部用yield返回值,引擎会自动补全迭代器所需的结构。改写后代码如下:
const scoreMap = {
math: 90,
english: 85,
science: 88,
*[Symbol.iterator]() {
for (const key of Object.keys(this)) {
yield [key, this[key]];
}
}
};
for (const [subject, score] of scoreMap) {
console.log(subject + ':' + score);
}
这里用到了对象方法简写加星号的生成器语法。Object.keys取出自身可枚举字符串键,每次yield抛出一个键值对数组。于是scoreMap可以直接解构成[key, value],也能被扩展运算符转为数组:const arr = [...scoreMap];。这种写法在需要处理配置对象、缓存对象时非常实用。
四、与内置语法的联动及注意事项
一旦对象可迭代,它不仅支持for...of,还能和大量语法糖配合。比如数组解构const [first, second] = obj,只要obj可迭代就能取前几个值;扩展运算符[..., obj]会把迭代结果铺平到新数组;甚至Promise.all在接收可迭代对象时也能正常工作。这让我们写的自定义数据结构可以无缝接入现有代码。
需要注意几个坑。第一,Symbol.iterator方法每次被调用都应返回全新的迭代器,否则多个循环共用同一指针会互相干扰。第二,如果对象已经部署了该方法,但返回的不是符合迭代器协议的对象,运行时照样报错。第三,用for...in遍历对象和用for...of遍历可迭代对象语义不同,前者拿键名且包含原型链,后者拿迭代值且只认协议。下面演示一个返回新迭代器避免状态污染的写法:
const counter = {
limit: 3,
[Symbol.iterator]() {
let count = 0;
const max = this.limit;
return {
next() {
if (count < max) {
return { value: count++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
const a = counter[Symbol.iterator]();
const b = counter[Symbol.iterator]();
console.log(a.next().value); // 0
console.log(b.next().value); // 0 互不影响
可以看到,每次访问counter[Symbol.iterator]都会重新执行函数,局部变量count独立存在,因此a和b两个迭代器各行其是。这样的实现才是安全的可迭代对象。
五、总结与实践建议
迭代器解决了“如何统一遍历”的问题,可迭代协议则让任意对象能接入语言级的循环与解构特性。普通对象虽默认不可迭代,但只需挂载Symbol.iterator并给出next或生成器,就能拥有和数组一样的遍历能力。在封装组件状态、树形结构、流式数据时,主动实现可迭代接口往往比暴露内部数组更优雅。
建议你在写工具库时,对暴露给外部的数据容器都考虑是否实现可迭代协议。不需要追求所有对象都可迭代,只针对那些确实有序列访问需求的场景,比如配置列表、队列、缓存条目。这样既能提升调用方代码的简洁度,也遵循了JavaScript现有的设计哲学。
iteratorSymbol_iteratoriterable_object修改时间:2026-08-02 08:12:32