整理了一些对比论和考点,可直接移步知识点梳理
面试原题
js
// 让下面的代码成立
var [a, b] = {
a: 3,
b: 4
}
console.log(a, b) // 3, 4
我们先直接执行这段代码,会得到什么呢
Uncaught TypeError: {(intermediate value)(intermediate value)} is not iterable
答案是会报错,因为对象不是可迭代的,所以解构赋值会报错
引发第一个思考
什么是可迭代对象
满足可迭代协议的对象,就是可迭代对象。
可迭代协议
- 对象属性有 Symbol.iterator, 值是函数,返回一个迭代器,迭代器的属性有 next 方法,返回一个对象,对象有 done 和 value 两个属性,done 表示是否迭代完成,value 表示迭代的值
- 一个对象既可以是 可迭代对象(拥有
[Symbol.iterator]
),又可以是迭代器(自身含有next()
方法)。
javascript
{
[Symbol.iterator]: function (){
return {
next:function () {
return {
value: value/undefined,
done: true/false => 完成/未完成
}
}
}
}
}
由此我们可以进行尝试,将题目的对象变成一个可迭代对象,然后解构赋值
js
var [a, b] = {
a: 3,
b: 4,
[Symbol.iterator]: function () {
// 利用数组的迭代器
return Object.values(this)[Symbol.iterator]()
}
}
console.log(a, b)
题目的要求不能改变源代码,所以我们可以在对象原型上添加 Symbol.iterator 属性,开始尝试
js
Object.prototype[Symbol.iterator] = function () {
return Object.values(this)[Symbol.iterator]()
}
var [a, b] = {
a: 3,
b: 4
}
console.log(a, b)
在 ES6 中,还有一个生成器,也可用于此
js
Object.prototype[Symbol.iterator] = function* () {
yield* Object.values(this)
}
ES6 内建可迭代类型
- Array、 String、 Map、 Set、 arguments、 NodeList DOM 集合
- 解构、for of、展开运算符(...)
- ECMAScript 的迭代机制基于可迭代协议统一了对数据结构的访问方式,使得各种集合类型数据结构可以通过一致的语法进行遍历。
追加下一个问题,什么是迭代器
在 JavaScrip 中有非常多的集合,比如数组、set、map等,他们是用来存放数据的集合。数据必然存在遍历,通常会使用for来遍历,就需要书写比较长的代码。官方就提供了一个属性 Iterable,并设定具有 Iterable 属性的数据结构就是可迭代的。
手搓一个迭代器
了解内部结构,助于理解迭代器原理
js
function MyIterator(arr) {
let i = 0;
return {
next: function () {
let done = i >= arr.length;
let value = !done ? arr[i++] : undefined;
return {
done,
value
}
}
}
}
let iterator = MyIterator([1, 2, 3]);
console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 }
console.log(iterator.next()); // { done: true, value: undefined }
for of
只要是拥有迭代器属性的数据结构,那么他就可以被 for of 遍历,也就是说我们只要在某个数据结构上添加迭代器属性,那么这个数据结构就可以被 for of 遍历。
for of 遍历的机制就是,先获取迭代器,然后通过 next 方法获取迭代的值,直到迭代完成,for of 遍历结束。
我们来看这段代码
js
let obj = {
value: 1
}
for (let item of obj) {
console.log(item)
}
这段代码的执行结果会报错,obj is not iterable, 这点其实也证实了 for of 遍历的是这个数据结构身上的迭代器属性,我们尝试给 obj 添加迭代器属性
js
let obj = {
value: 1,
}
obj[Symbol.iterator] = function () {
return MyIterator([1, 2])
}
for (let item of obj) {
console.log(item) // 1, 2
}
可以看到 obj 可以被 for of 遍历了,但是并不是遍历的 Obj 身上的属性,而是传进去的数组,因为 for of 遍历的是 obj 上的迭代器对象。
继续手撕一个 for of
for of 首先检索对象身上是否有迭代器属性,如果没有则会报错,如果有,则通过 next 方法获取迭代的值,直到迭代完成,for of 遍历结束。
js
function myForOf(obj, callback) {
if (!obj[Symbol.iterator]) {
throw new Error(obj + ' is not iterable');
}
let iterator = obj[Symbol.iterator]();
let item = iterator.next();
while (!item.done) {
callback(item.value);
item = iterator.next();
}
}
let arr = [1, 2, 3];
myForOf(arr, (item) => {
console.log(item);
})
持续追问,什么是生成器(Generator)
- 生成器是 ES6 引入的一种特殊函数形式,具备"暂停执行"和"恢复执行"的能力。它不是一次性执行到底,而是通过
yield
分段执行,由调用者控制流程。 - 生成器的本质是状态机,其内部维护着上下文信息,在多个
yield
之间保持函数状态,是构建复杂控制流(如携程、异步流程控制)的基础
基本语法
1.定义生成器函数
js
function* generatorFunction() {
yield 'hello';
yield 'world';
}
- 使用
function*
定义 - 使用
yield
暂停和返回数据
2. 调用方式
js
let generator = generatorFunction();
console.log(generator.next()); // { value: 'hello', done: false }
console.log(generator.next()); // { value: 'world', done: false }
console.log(generator.next()); // { value: undefined, done: true }
生成器的工作机制
关键词 | 说明 |
---|---|
yield | 暂停函数执行,返回数据 |
next() | 恢复函数执行,传入数据给上一个 yield 表达式 |
done | 表示函数是否执行完毕 |
return() | 提前结束生成器,并返回值 |
throww() | 向生成器内部抛出异常 |
示例:带参数的 .next()
js
function* calc() {
const x = yield 10;
const y = yield x + 5;
return y;
}
const it = calc();
console.log(it.next()); // { value: 10, done: false }
console.log(it.next(20)); // { value: 25, done: false }
console.log(it.next(99)); // { value: 99, done: true }
生成器 vs 普通函数
特性 | 普通函数 | 生成器函数 |
---|---|---|
是否可以中断执行 | 否 | 是(yield 暂停) |
是否可多次返回值 | 否 | 是(通过多次 yield) |
返回值类型 | 任意 | Iterator 迭代器对象 |
控制权 | 函数内部 | 调用方可控制执行流程 |
生成器的用途与应用场景
-
惰性求值/无线序列
节省内存,在需要的时候再生成结果
jsfunction* fibonacci() { let i = 0; while (true) yield i++; }
-
自定义迭代器
jsconst MyIterator = { *[Symbol.iterator]() { yield 1; yield 2; yield 3; } }
-
控制异步流程
jsfunction* getData() { const user = yield fetchUser(); const posts = yield fetchPosts(user.id); }
常见面试高频问题
生成器和 async/await 的区别
特性 | Generator | async/await |
---|---|---|
本质 | 可中断函数 | 异步语法糖 |
是否返回 Promise | 否 | 是 |
控制流程 | 手动调用 .next() |
自动执行 |
场景 | 同步流程、数据流 | 异步流程 |
yield*
的作用?
委托执行另一个生成器或可迭代对象
js
function* inner() {
yield 1;
yield 2;
}
function* outer() {
yield 'start';
yield* inner(); // 委托 inner 的全部 yield
yield 'end';
}
注意事项
- 生成器不能用箭头函数(
function*
是关键字) - 生成器本质是 "暂停式函数",但每次恢复执行时上下文仍然保留
yield
只能在生成器函数中使用- 每次调用
next()
都会推进一次yield
- 可以使用
return()
提前终止生成器。 - 用
throw()
可以向生成器内部抛异常。
生成器速记口诀
vbnet
function 星星起手式,yield 中断给值时;
next 叫醒继续跑,return throw 提前跑;
惰性状态都能搞,async 之前它称王。
生成器与迭代器
生成器是"自动构建迭代器的工厂函数"
js
function* gen() {
yield 1;
yield 2;
}
const it = gen();
typeof it.next === 'function'; // true
it[Symbol.iterator] === it; // true
- 生成器返回的对象天然遵守迭代器协议
- 同时也是可迭代对象
生成器 + 迭代器组合使用场景
实现复杂自定义迭代器(优雅替代next()写法
)
js
const obj = {
data: [1, 2, 3],
* [Symbol.iterator]() {
for (let item of this.data) {
yield item * 2;
}
}
}
for (let item of obj) {
console.log(item); // 2, 4, 6
}
用生成器实现延迟计算(惰性求值)
js
function* lazyMap(arr, fn) {
for (let item of arr) {
yield fn(item);
}
}
const square = lazyMap([1, 2, 3], x => x * x);
for (let item of square) {
console.log(item); // 1, 4, 9
}
避免无限循环内存爆炸
js
function* forMap() {
let i = 0;
while (true) {
yield i++;
}
}
// 通过 for of 控制迭代数量
let count = 0;
for (let item of forMap()) {
if (count++ > 10) break;
console.log(item);
}
多层遍历嵌套 yield*
委托
js
function* gen() {
yield 1;
yield* inner();
yield 3;
}
function* inner() {
yield 2;
}
for (let item of gen()) {
console.log(item); // 1, 2, 3
}
yield*
可委托数组、Set、Map、字符串、生成器等可迭代对象
知识点梳理
一、基础识记类
问题 | 答案要点 |
---|---|
什么是可迭代协议? | 实现了 Symbol.iterator() 方法,返回一个迭代器。 |
什么是迭代器协议? | 拥有 .next() 方法,返回 { value, done } 。 |
for...of 要求什么? |
对象必须是 iterable(实现 Symbol.iterator() )。 |
Symbol.iterator 和 next() 区别? |
Symbol.iterator() 返回迭代器,.next() 控制执行。 |
如何判断可迭代? | typeof obj[Symbol.iterator] === 'function' |
yield 作用? |
暂停执行,向外返回值,可双向通信。 |
二、 原理机制类
问题 | 答案要点 |
---|---|
生成器 vs 普通函数? | 可暂停、返回迭代器、使用 yield 。 |
为什么生成器是状态机? | yield 为状态切换点,每次 .next() 推进状态。 |
yield 和 .next() 怎么通信? |
.next(val) 把 val 作为上一个 yield 的返回值。 |
yield* 作用? |
委托另一个可迭代对象的迭代过程。 |
手动实现生成器执行器? | 递归调用 .next() 推进流程。 |
为什么说生成器是迭代器工厂? | 返回对象即是迭代器,实现自定义控制流程。 |
三、 实战应用类
应用场景 | 示例 |
---|---|
无限整数流 | function* infinite() { let i = 0; while(true) yield i++; } |
惰性 map | function* lazyMap(arr, fn) { for (let a of arr) yield fn(a); } |
给对象加迭代能力 | *[Symbol.iterator]() { for (let d of this.data) yield d; } |
分帧执行 | for (let i=0; i<10000; i++) { yield; } |
模拟 async/await | 使用 run(gen) 推进器配合 Promise 实现异步控制 |
四、 注意事项类
问题 | 答案要点 |
---|---|
生成器能用箭头函数吗? | 不能,箭头函数不支持 yield 。 |
yield 可在普通函数中用吗? |
不可,语法错误。 |
.return() 和 .throw() ? |
.return(val) 结束迭代;.throw(err) 抛异常。 |
提前终止生成器后果? | 后续 yield 不再执行,注意资源清理(用 finally )。 |
五、对比迁移类
生成器 vs async 函数
对比项 | 生成器 | async 函数 |
---|---|---|
暂停方式 | yield |
await |
执行控制 | 手动 .next() |
自动推进 |
异步控制 | 需结合执行器和 Promise | 内建异步控制 |
返回值 | 迭代器对象 | Promise |
生成器 vs Promise
对比项 | 生成器 | Promise |
---|---|---|
控制的是 | 执行流程 | 异步值 |
执行方式 | 手动推进 | 自动执行 |
状态 | 多次暂停/恢复 | 单次完成/拒绝 |
可迭代对象 vs 类数组对象
对比项 | 可迭代对象 | 类数组对象 |
---|---|---|
是否可 for...of |
可 | 不可(除非手动实现) |
是否有 Symbol.iterator |
可 | 不可 |
是否有索引和 length | 不一定 | 一定有 |