迭代(Iterator)与生成器(Generator)是 ES6 高阶核心语法,是 JS 实现统一遍历协议、异步流程控制、惰性求值的底层支撑。绝大多数开发者只会简单使用 for 循环,却不懂遍历底层原理、迭代协议、生成器双向通信,这也是中高级前端、大厂面试高频难点。
本文全覆盖指定知识点:for...of vs for...in 核心差异、Symbol.iterator 迭代协议、原生可迭代对象、yield 高级用法、生成器双向通信,从零吃透迭代体系底层逻辑。
一、遍历与迭代的核心区别
很多人混淆遍历和迭代,二者底层逻辑完全不同:
-
遍历:单纯逐个获取集合元素,无统一规范(for、forEach、for...in),不同数据结构遍历规则不统一
-
迭代:基于统一迭代协议,通过迭代器对象逐个产出数据,惰性取值,可控、可暂停、可终止
核心结论:for...in 是遍历,for...of 是迭代(遵循迭代协议)。
二、for...in vs for...of(面试必考对比)
两种循环看似都是遍历,底层机制、适用场景、坑点完全不同,是前端基础高频面试题。
2.1 for...in(遍历键名)
专门用于遍历对象可枚举属性键名,不推荐遍历数组!
核心特性
-
遍历自身+原型链上的可枚举属性
-
遍历数组时,索引为字符串类型,非数字
-
遍历顺序不保证有序
-
无法遍历 Symbol 类型属性
const arr = [10, 20, 30]
arr.name = '数组'
// 遍历索引(字符串)+ 自定义属性
for(const key in arr) {
console.log(key) // '0'、'1'、'2'、'name'
}
2.2 for...of(迭代值)
ES6 全新迭代循环,遵循迭代协议,只能遍历可迭代对象,是数组、集合、类数组遍历首选。
核心特性
-
直接遍历值,而非键名
-
不遍历原型属性、不遍历自定义可枚举属性
-
支持 break、continue、return(可中断)
-
支持遍历 Symbol 属性
-
自动抹平数组空位(empty → undefined)
const arr = [10, 20, 30]
for(const item of arr) {
console.log(item) // 10、20、30
}
2.3 终极对比总结
| 特性 | for...in | for...of |
|---|---|---|
| 遍历内容 | 键名/索引 | 元素值 |
| 遍历原型属性 | ✅ 会遍历 | ❌ 不遍历 |
| 中断循环 | ✅ 支持 | ✅ 支持 |
| 适用场景 | 纯普通对象遍历 | 数组、Map、Set、类数组 |
| 迭代协议 | 不遵循 | 遵循 Symbol.iterator 协议 |
三、Symbol.iterator 迭代协议(底层核心)
可迭代对象的本质:只要对象部署了 Symbol\.iterator 方法,就是可迭代对象,可被 for...of、扩展运算符、Array.from、解构遍历。
3.1 迭代器运行机制
-
调用对象的
\[Symbol\.iterator\]\(\)方法,生成迭代器对象 -
迭代器拥有唯一核心方法
next\(\) -
每次调用 next() 返回结构:
\{ value: 当前值, done: 是否遍历完毕 \} -
done 为 true 时,迭代结束
3.2 手动实现迭代器
const arr = [1,2,3]
// 获取迭代器
const iterator = arr[Symbol.iterator]()
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true } 迭代结束
3.3 自定义可迭代对象(面试手写)
普通对象默认不可迭代,手动部署迭代协议,即可被 for...of 遍历:
const obj = {
data: [10, 20, 30],
// 部署迭代协议
[Symbol.iterator]() {
let index = 0
const data = this.data
return {
next() {
return index < data.length
? { value: data[index++], done: false }
: { value: undefined, done: true }
}
}
}
}
// 可正常迭代
for(const item of obj) {
console.log(item) // 10、20、30
}
四、原生可迭代对象大全
以下原生数据结构默认部署 Symbol.iterator,天然支持 for...of 迭代:
-
Array 数组:最常用可迭代对象
-
String 字符串:逐字符迭代
-
Map 集合:迭代键值对,有序
-
Set 集合:迭代元素值,去重有序
-
arguments:函数内置类数组可迭代对象
-
NodeList/HTMLCollection:DOM 查询结果集合
4.1 特殊可迭代对象遍历示例
// Map 迭代
const map = new Map([['name','张三'],['age',20]])
for(const [key, val] of map) {
console.log(key, val)
}
// Set 迭代
const set = new Set([1,2,2,3])
for(const item of set) {
console.log(item)
}
// arguments 迭代
function fn() {
for(const arg of arguments) {
console.log(arg)
}
}
fn(100,200)
五、Generator 生成器基础
生成器函数:ES6 提供的可暂停、可恢复特殊函数,是迭代器的语法糖,自带迭代协议,专门用于实现惰性迭代、异步流程控制。
5.1 核心特征
-
函数声明带
function\*标识 -
内部通过
yield暂停代码执行 -
调用生成器函数,不执行代码,仅返回迭代器对象
-
通过
next\(\)恢复执行,分段产出值
5.2 基础语法
function* gen() {
yield 1
yield 2
yield 3
return '结束'
}
const g = gen()
console.log(g.next()) // {value:1, done:false}
console.log(g.next()) // {value:2, done:false}
console.log(g.next()) // {value:3, done:false}
console.log(g.next()) // {value:'结束', done:true}
六、yield 高级用法
yield 不只是单纯暂停,具备表达式运算、值接收、嵌套迭代等高级能力,是生成器核心难点。
6.1 yield 表达式返回值
重点:yield 产出的值是 next().value,yield 本身的返回值是下次 next 传入的参数。
function* gen() {
// res 接收第二次 next 传入的参数
const res = yield 100
console.log(res) // 200
yield 200
}
const g = gen()
g.next() // 产出100
g.next(200) // 给上一次 yield 赋值,产出200
6.2 yield* 委托迭代(嵌套迭代)
yield\* 用于委托迭代其他可迭代对象/生成器,简化嵌套迭代写法。
function* inner() {
yield 'a'
yield 'b'
}
function* outer() {
yield 1
yield* inner() // 委托迭代内部生成器
yield 2
}
// 迭代结果:1 a b 2
for(const item of outer()) console.log(item)
七、生成器双向通信(顶级难点)
普通迭代是单向取值,生成器支持双向通信:外部可以向生成器内部传递数据、抛出异常、强制终止,实现内外数据交互,是 Generator 控制异步流程的核心原理。
7.1 正向通信:next() 传参
外部通过 next\(val\) 向生成器内部传入值,赋值给上一次 yield 表达式,实现数据从外部流向内部。
function* gen() {
console.log('启动')
let num1 = yield '等待参数1'
console.log('接收参数1:', num1)
let num2 = yield '等待参数2'
console.log('接收参数2:', num2)
}
const g = gen()
console.log(g.next()) // 启动、{value: '等待参数1', done: false}
console.log(g.next(666)) // 接收参数1:666、{value: '等待参数2', done: false}
console.log(g.next(888)) // 接收参数2:888、{value: undefined, done: true}
注意:第一次 next() 传参无效,没有上一次 yield 接收参数。
7.2 异常通信:throw() 抛错
外部可通过 throw\(\) 向生成器内部抛出错误,内部通过 try/catch 捕获,实现异常双向处理。
function* gen() {
try {
yield 1
yield 2
} catch (err) {
console.log('内部捕获错误:', err)
}
}
const g = gen()
g.next()
// 外部抛错,内部捕获
g.throw('程序异常!')
7.3 强制终止:return() 结束迭代
return\(val\) 强制终止生成器迭代,直接修改 done 为 true,后续 yield 不再执行。
function* gen() {
yield 1
yield 2
yield 3
}
const g = gen()
console.log(g.next()) // {value:1, done:false}
console.log(g.return(999))// {value:999, done:true} 强制结束
console.log(g.next()) // {value:undefined, done:true}
7.4 双向通信核心价值
-
实现分步异步传参,替代嵌套回调
-
精准控制异步流程的暂停、恢复、终止、异常捕获
-
是 async/await 的底层实现原理(Generator + Promise 自动化执行)
八、迭代与生成器工程实战场景
-
惰性求值:大数据分步迭代,避免一次性加载阻塞内存
-
异步流程控制:顺序执行多个异步请求,替代回调地狱
-
无限序列生成:生成无限递增数字、随机数等序列
-
数据分片遍历:长列表、大文件分片迭代处理
-
状态机实现:通过 yield 暂停切换不同状态
九、全文核心总结(面试必背)
-
for...in:遍历键名、遍历原型属性、不可迭代协议,仅用于普通对象
-
for...of:遍历值、遵循迭代协议、可中断、不遍历原型属性,是遍历集合首选
-
Symbol.iterator:迭代协议核心,部署该方法即为可迭代对象
-
原生可迭代对象:Array、String、Map、Set、arguments、NodeList
-
yield:暂停生成器执行,支持表达式传值,yield* 可委托嵌套迭代
-
生成器双向通信:next传参、throw抛错、return终止,实现内外数据交互,是异步终极方案底层
十、高频面试简答题
-
for...in 和 for...of 的区别? for...in 遍历键名、遍历原型属性、不可中断(可中断但不推荐)、无迭代协议;for...of 遍历值、遵循迭代协议、可中断、不遍历原型属性,适配所有可迭代对象。
-
**什么是可迭代对象?**部署了 Symbol.iterator 方法,可生成迭代器、被 for...of/扩展运算符遍历的对象。
-
yield 和 yield* 的区别? yield 用于暂停单次执行、产出单个值;yield* 用于委托迭代其他可迭代对象,实现嵌套迭代。
-
生成器双向通信原理? 通过 next() 向内部传参、throw() 内部抛错、return() 终止迭代,实现内外双向数据交互与流程控制。
-
Generator 和 async/await 的关系? async/await 是 Generator + Promise 的语法糖,解决了 Generator 需要手动 next() 的痛点,实现异步自动执行。