介绍
本文是 JavaScript 高级深入浅出的第 16 篇,本文将会介绍生成器与迭代器相关知识
正文
1. 迭代器
1.1 什么是迭代器
迭代器(Interator)是确使用户可在容器对象(container,如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。
- 其行为像数据库中的光标,迭代器最早出现在 1974 年设计的 CLU 编程语言中
- 在各种编程语言的实现中,迭代器的实现方式各不相同,但是很多语言都有迭代器,如 Java、Python 等
从迭代器的定义来看,迭代器是帮助我们对某个数据结构进行遍历的对象
在 JS 中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol)
- 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式
- 这个在 JS 的标准中就是一个特定的
next方法
1.2 next 方法规范
next 方法有如下的要求:
- 一个无参或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
done(boolean)- 如果迭代器可以产生序列中的下一个值,则为
false(等价于没有done这个属性) - 如果迭代器已将序列迭代完毕,则为
true,这种情况下,value是可选的,如果它依然存在,则为迭代结束之后的默认值
- 如果迭代器可以产生序列中的下一个值,则为
value- 迭代器返回的任何 JS 值,
done为true时可以省略
- 迭代器返回的任何 JS 值,
1.3 迭代器例子
const names = ['Alex', 'John', 'Alice']
// 创建一个迭代器对象来访问数组
let index = 0
const namesIterator = {
next() {
if (index < names.length) {
return { done: false, value: names[index++] }
} else {
return { done: true, value: undefined }
}
},
}
console.log(namesIterator.next()) // { done: false, value: 'Alex' }
console.log(namesIterator.next()) // { done: false, value: 'John' }
console.log(namesIterator.next()) // { done: false, value: 'Alice' }
console.log(namesIterator.next()) // { done: true, value: undefined }
1.4 可迭代对象
上面的代码看起来会很奇怪:
- 我们在获取数组的时候,需要自己创建一个
index变量,再创建一个所谓的可迭代对象 - 事实上我们可以对上面的代码进行封装,让其变成一个可迭代对象
什么又是可迭代对象呢?
- 和迭代器是不同的概念
- 当一个对象实现了
iterator protocol迭代器协议时,它就是一个可迭代对象 - 这个对象的要求时必须实现
@@iterator方法,在代码中我们使用Symbol.iterator访问
const namesIterator = {
names: ['Alex', 'John', 'Alice'],
[Symbol.iterator]() {
let index = 0
return {
next: () => {
if (index < this.names.length) {
return { done: false, value: this.names[index++] }
}
return { done: true, value: undefined }
},
}
},
}
1.5 原生迭代器对象
我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象:
String、Array、Map、Set、arguments对象、NodeList集合
const names = ['Alex', 'John', 'Alice']
const namesIterator = names[Symbol.iterator]()
console.log(namesIterator.next()) // { value: 'Alex', done: false }
console.log(namesIterator.next()) // { value: 'John', done: false }
console.log(namesIterator.next()) // { value: 'Alice', done: false }
console.log(namesIterator.next()) // { value: undefined, done: true }
1.6 可迭代对象的应用
- JS 中的语法:
for...of、展开语法、yield*、解构赋值 - 创建一些对象时:
new Map([iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]) - 一些方法的调用:
Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable)
1.7 自定义类的可迭代性
Array、Set、String、Map等创建出来的对象都是可迭代对象
- 在面向对象的开发中,也可以自己创建一个类,用于创建可迭代对象
案例:创建一个 classroom 类
- 教室有自己的属性(名称、最大容量、当前教室的学生)
- 可以进入新的学生
push- 创建的教室对象都是可迭代对象
class Classroom {
constructor(name, maxCount, currentStudents) {
this.name = name
this.maxCount = maxCount
this.currentStudents = currentStudents
}
push(student) {
if (this.maxCount === this.currentStudents.length) return
this.currentStudents.push(student)
}
[Symbol.iterator]() {
let index = 0
return {
next: () => {
if (index < this.currentStudents.length) {
return { done: false, value: this.currentStudents[index++] }
}
return { done: true, value: undefined }
},
// 通过 return 可以监听跳出操作
return: () => {
console.log('迭代器 break')
// 记得要 return
return { done: true, value: undefined }
},
}
}
}
const c1 = new Classroom('6-1', 40, ['Lily', 'Alex', 'John', 'Jason'])
for (const student of c1) {
console.log(student)
if (student === 'Alex') break
}
// Lily
// Alex
// 迭代器 break
2. 生成器
2.1 什么是生成器
生成器是 ES6 新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。
正常的函数,终止的条件通常是返回值或出现异常。
生成器函数也是一个函数,但是和普通的函数有一些区别:
- 首先,生成器函数需要在
function后面加一个* - 其次,生成器函数可以通过
yield关键字来控制函数的执行流程 - 最后,生成器函数的返回值是一个
Generator生成器,生成器是一种特殊的迭代器
function* foo() {
console.log('函数开始执行')
const value1 = 100
console.log(value1)
yield
const value2 = 200
console.log(value2)
yield
const value3 = 300
console.log(value3)
yield
console.log('函数执行完毕')
}
// 在调用生成器函数时,函数内一行代码也不会执行
// 而是返回一个生成器对象
const generator = foo()
// 通过 next 方法来调用每一段 yield 之前的代码
generator.next()
// 函数开始执行
// 100
generator.next()
// 200
generator.next()
// 300
generator.next()
// 函数执行完毕
2.2 生成器函数的返回值
迭代器在执行 next 方法后,是可以返回 { done: Boolean, value: '' },在生成器函数的执行过程中,也是可以返回数据的
function* foo() {
yield
return 'finish'
}
const generator = foo()
console.log(generator.next()) // { value: undefined, done: false }
console.log(generator.next()) // { value: 'finish', done: true }
由此我们可以知道,在生成器函数中 return 的值,将会作为 done 时的 value
那么我们该如何控制每一步执行的 value 呢?yield 后面可以跟随一个值/表达式,将会作为每一步的 value
function* foo() {
yield 'first'
yield 'second'
return 'finish'
}
const generator = foo()
console.log(generator.next()) // { value: 'first', done: false }
console.log(generator.next()) // { value: 'second', done: false }
console.log(generator.next()) // { value: 'finish', done: true }
2.3 生成器的其他方法使用
1. next 传递参数
我们在调用生成器的 next 方法时,可以传入参数,那么该参数会在哪里被接收呢?
function* foo() {
console.log('第一段代码')
// 会在这里接收参数
const input = yield
console.log('input', input)
console.log('第二段代码')
}
const generator = foo()
generator.next()
// 第一段代码
generator.next('输入')
// input 输入
// 第二段代码
我们在调用 next() 时传入的参数,将会作为该段代码的上一个 yield 的返回值。
那第一段代码怎么拿到参数呢?可以给生成器函数传入实参。
function* foo(input) {
console.log('input', input)
}
const generator = foo('传入参数')
generator.next() // input 传入参数
2. 生成器提前结束 - return 方法
function* foo() {
console.log('第一段代码')
const input = yield
console.log('第二段代码')
console.log('input', input)
yield
console.log('第三段代码')
}
const generator = foo()
generator.next() // 第一段代码
const returnValue = generator.return('传入参数')
console.log('returnValue', returnValue) // returnValue { value: '传入参数', done: true }
generator.next() // 不会执行
由上面的代码可以得出,我们可以使用 return 方法来提前中断生成器的执行
const input = yield
return input // 相当于直接在这里添加了一行代码 return input
3. 生成器抛出异常 - throw 方法
function* foo() {
console.log('第一段代码')
yield
console.log('第二段代码')
yield
console.log('第三段代码')
}
const generator = foo()
generator.next() // 第一段代码
generator.throw() // 有了这一段代码,直接报错
generator.next() // 不会执行
由上面的代码可以得出,我们可以使用 return 方法来提前中断生成器的执行,相当于在第一个 yield 的地方抛出了一个异常,需要捕获才会执行下面的代码
function* foo() {
console.log('第一段代码')
// 这里捕获异常
try {
yield
} catch (error) {}
console.log('第二段代码')
yield
console.log('第三段代码')
}
const generator = foo()
generator.next() // 第一段代码
generator.throw() // 第二段代码
generator.next() // 第三段代码
// 就可以正常执行了
throw方法也可以传参,参数会被 catch 的参数接收
2.4 生成器替代迭代器
生成器是一种特殊的迭代器,因此在某种应用场景下可以直接使用生成器来替代迭代器
// 迭代器代码
const namesIterator = {
names: ['Alex', 'John', 'Alice'],
[Symbol.iterator]() {
let index = 0
return {
next: () => {
if (index < this.names.length) {
return { done: false, value: this.names[index++] }
}
return { done: true, value: undefined }
},
}
},
}
// 生成器代码
const names = ['Alex', 'John', 'Alice']
function* generator(names) {
for (const item of names) {
yield item
}
}
const gen = generator(names)
console.log(gen.next()) // { value: 'Alex', done: false }
console.log(gen.next()) // { value: 'John', done: false }
console.log(gen.next()) // { value: 'Alice', done: false }
console.log(gen.next()) // { value: undefined, done: true }
事实上我们还可以通过 yield* 生产一个可迭代对象(这个是 yield 的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中一个值)
// 使用 yield*
const names = ['Alex', 'John', 'Alice']
function* generator(names) {
yield* names
}
const gen = generator(names)
console.log(gen.next()) // { value: 'Alex', done: false }
console.log(gen.next()) // { value: 'John', done: false }
console.log(gen.next()) // { value: 'Alice', done: false }
console.log(gen.next()) // { value: undefined, done: true }
2.5 自定义类的生成器方案
我们可以使用生成器来重写 1.7 的案例
class Classroom {
constructor(name, maxCount, currentStudents) {
this.name = name
this.maxCount = maxCount
this.currentStudents = currentStudents
}
push(student) {
if (this.maxCount === this.currentStudents.length) return
this.currentStudents.push(student)
}
// 如果没有 function 关键字,可以在方法名前面写
*[Symbol.iterator]() {
yield* this.currentStudents
}
}
const c1 = new Classroom('6-1', 40, ['Lily', 'Alex', 'John', 'Jason'])
for (const student of c1) {
console.log(student)
}
// Lily
// Alex
// John
// Jason
3. Promise 的缺陷
在第 15 篇中,介绍了 Promise 是一种异步代码的规范,Promise 真的那么好吗?诚然,它使用起来是非常友好的,但是在某种情况下,还是可能会出现嵌套地狱的情况:
需求:需要多次请求接口,多个接口返回的数据结合在一起,打印最终数据
// requestData 返回一个 Promise
requestData("alex").then(res => {
requestData(res + 'bbb').then(res2 => {
requestData(res + 'ccc').then(res3 => {
requestData(res3 + 'ddd').then(res4 => {
// .....
})
})
})
})
如果出现上述的场景下,多个嵌套之间可能还会有其他的复杂的业务逻辑,那么还是会出现回调地狱,所以该如何解决呢?
3.1 解决方案一
requestData("alex").then(res => {
return requestData(res + 'aaa')
}).then(res => {
return requestData(res + 'bbb')
}).then(res => {
return requestData(res + 'ccc')
}).then(res => {
// .....
})
这种方式,可以解决嵌套的问题,但是可读性还是很差的,如果掺杂着逻辑代码,也会看起来很复杂
3.2 解决方案二
这种解决方案,决定采用 Promise + generator 的方式
function* getData(params) {
const params1 = yield requestData(params)
const params2 = yield requestData(params1)
const params3 = yield requestData(params2)
const params4 = yield requestData(params3)
console.log(params4)
}
const generator = getData('alex')
// 写一个函数,自动执行生成器
function execGeneratorFn(genFn) {
const generator = genFn()
function exec(params) {
const result = generator.next(params)
if (result.done) {
return result.value
}
result.value.then(res => {
exec(res)
})
}
exec()
}
execGeneratorFn(getData)
3.3 解决方案三
我们上述实现的自动执行生成器的代码,在社区中已经有库了 co
npm install co
const co = require('co')
co(getData) // 这种方式同样也可以
3.4 使用 async/await
自 ES8 开始,JS 支持自动执行生成器
async function getData() {
const params1 = await requestData('alex')
const params2 = await requestData(params1 + 'aaa')
const params3 = await requestData(params2 + 'bbb')
const params4 = await requestData(params3 + 'ccc')
console.log(params4)
}
getData() // 没问题
所以我们就可以知道,其实 async/await 本质上还是 Promise + Generator 的语法糖
总结
本文讲了什么是生成器与迭代器,并通过多个例子来演示。同时,也通过生成器 + Promise 最终引出来 async/await
在下一篇中,将会重点介绍 async、await 以及事件循环