迭代器和生成器
迭代器: 一种通用的迭代方式
现有迭代模式
在开始了解迭代器之前,我们先回顾一下现有迭代方式的缺点
css
for(let i = 0; i < num; i){}
for循环是最简单的迭代,这种迭代方式有两个明显的缺点:
- 迭代之前需要知道如何使用数据结构: 迭代时只能先拿到数组的引用,然后使用
[]
来访问数据 - 迭代的数据结构相对固定:通过递增所索引来访问数据只能用于数组
es5
新增了Array.prototype.forEach
,向通用的迭代前进了一步,解决了单独记录索引和通过数组对象取值的问题,但依然有缺点:
- 不能标识迭代何时中止
在其他语言中使用的模式可以使开发者不需要知道如何迭代而进行迭代操作,这种模式就是迭代器模式
,JS
在es6
之后也支持了这种模式
迭代器模式
一种方案,描述什么是可迭代对象:实现了Iterable
接口,且该可迭代对象可以通过迭代器Iterator
进行消费.
可迭代协议
描述什么是Iterable
接口: 以[Symbol.Iterator]
为键,以迭代器工厂函数为值,调用这个函数返回一个迭代器对象
该属性作为默认迭代器,再迭代时会先调用该工厂函数,获取到迭代器,然后通过迭代器进行迭代
迭代器协议
描述什么是迭代器: 一种一次性使用的对象,该对象必须包含一个next
属性,属性值为一个方法next()
,每次调用该方法都会返回一个迭代结果对象result
,result
对象包含两个属性{value: any,done: Boolean}
,value
表示当此迭代的值,done
表示值是否迭代完毕.
找个迭代器实际看一下:
lua
以数组为例,数组自带迭代器接口:
const arr = ['1','2','3'];
const iterator = arr[Symbol.iterator]() // 获取到迭代器对象
// 使用迭代器对象的next方法进行迭代
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}
console.log(iterator.next()) // {value: undefined,done: true}
每调用一次接口生成的迭代器对象都是独立的
自带迭代器的数据结构
js为很多内置类型都实现了迭代器接口,这些数据结构自带默认的迭代器结构,迭代时无需自定义,:
- 字符串 String
- 数组 Array
- 映射 Map
- 集合 Set
- NodeList DOM集合类型
- arguments
隐式调用[Symbol.iterator]
在实际迭代的时候部分语法会隐式
调用迭代器接口获取迭代器对象,然后使用迭代器进行迭代,这些方法有:
- for of
- 数组解构
- 扩展运算符
- Array.form()
- 创建集合
- 创建映射
- Promise.all(),接收以Promise组成的可迭代对象
- Promise.race,接收以Promise组成的可迭代对象
- yield*操作符,生成器中使用
自定义迭代器
说了这么多概念,现在来实操一下: 将任何一个object
,转换成可迭代对象:
javascript
function addIterator(obj){
const objCopy = _.deepCopy(obj)
Object.defineProperty(objCopy,Symbol.iterator,{
enumerable: false,
writable: false,
configurable: true,
value: function(){
const keys = Object.keys(obj);
let i = 0;
const len = keys.length;
return {
next: function() {
return {
value: obj[keys[i++]],
done: i > len
}
}
}
}
})
return objCopy
}
let obj = {a: '1',b: '2'};
obj = addIterator(obj) // 获取添加了迭代器接口的对象
const iterator = obj[Symbol.iterator]() // 获取迭代器
// 尝试打印一下
console.log(iterator.next()) // {value: 1,done: false}
console.log(iterator.next()) // {value: 2,done: false}
console.log(iterator.next()) // {value: undefined,done: true}
for(let value of obj){
console.log(value) // 1,2
}
提前中止迭代器
迭代器对象还有一个可选的return
方法,在迭代器,在迭代提前退出时调用,并且该方法必须返回一个有效的IteratorResult
对象,简单情况下也可以返回一个{done: true}
那么什么叫做提前退出呢? 包括一下情况:
- for-of循环通过
break
,continue
,return
,throw
提前退出的情况 - 解构操作并未消费所有的值
对这几种情况来实验一下:
kotlin
以break为例子:
const arr = [1,2,3,4,5,6];
const iterator = arr[Symbol.iterator]()
iterator.return = function(){
console.log('提前退出')
return {done: true}
}
for(let val of iterator){
if(val === 2){
break
}
console.log(val)
}
// 输出:
// 1
// 提前退出
不仅如此迭代器还有关闭的概念,如果一个迭代器没有关闭,还可以继续从上次迭代的地方继续迭代,被关闭了则不可以.JS中有的迭代器可以关闭,有的迭代器不会关闭,比如数组:
kotlin
const arr = [1,2,3,4];
const iterator = arr[Symbol.iterator]()
iterator.return = function(){
console.log('提前退出')
return {done: true}
}
for(let val of iterator){
if(val === 2){
break
}
console.log(val)
}
// 1
// 提前退出
for(let val of iterator){ // 由于数组的迭代器不能关闭,本次的迭代会继续上次的位置开始
if(val === 2){
break
}
console.log(val)
}
// 3
// 4
需要注意的是迭代器能否关闭和有无return方法无关
生成器 Generator
了解生成器之前先思考一下:
- 函数执行的过程中可以返回多个值吗?
- 函数执行的过程中可以接收外界的参数吗?
正常函数当然是不可以的,但生成器函数可以实现这两点,因为他有特殊的关键字yield
,下面我们来研究一下它是怎么做到的
什么是生成器
一个由function
和*
定义的函数,调用这个函数返回一个迭代器对象
,并且函数内部可以使用关键字yield
暂停代码的执行:
javascript
执行代码遇到yield将暂停函数的执行,并且函数的上下文不会被销毁
function* generatorFn(){
yield 1;
return 0;
}
生成器函数具有以下特征:
- 调用生成器函数不会立即执行函数内部代码 ,而是返回一个
生成器对象
,包含迭代器对象
的所有特征,因此生成器对象可以作为迭代器对象使用 - 调用
生成器对象
的next
方法才会执行生成器函数中的代码,且next
函数返回结果为iteratorReault
对象:{value:any,done: Boolean}
,value
表示执行结果,done
表示迭代是否完毕 - 执行代码时,遇到
yield
关键字,将暂停函数的执行,并将yield
后的表达式的值返回给iteratorReault
的value
属性,本次next
函数执行完毕 - 再次调用
next
方法时,生成器函数会从上个yield
暂停的地方继续执行...循环往复 - 直到没有
yield
或遇到return
时,done
自动变为true
,表示生成器函数执行完毕,之后调用next
都返回{value:undefined,done: true}
lua
function* generatorFn(){
yield 1;
yield 2;
yield 3
return 0;
}
const generator = generatorFn() // 获取生成器对象
generator.next() // 开始执行生成器函数内部代码,遇到yield关键字结束,并返回yield后表达式的值 这里输出 {value: 1,done: false}
generator.next() // 继续从上次暂停的yield向下执行,遇到第二个yield暂停执行,输出 {value: 2,done: false}
generator.next() // 从第二个yield之后开始执行,遇到第三个yield暂停执行,输出{value: 3,done: false}
generator.next() // 从第三个yield之后开始执行,遇到renturn 输出{value: 0,done: true}
generator.next() // 已经没有代码可以执行了,之后调用next都将返回{value: undefined,done:true}
总结一下:调用生成器函数
返回 一个生成器对象
,每调用一次生成器对象
的next
方法就会将生成器函数
内的代码执行到下一个yield
,并返回一个{value:any,done:boolean}
对象,value
表示yield
后表达式的值,done
表示迭代是否结束,继续调用将重复上述步骤
yield关键字
yield
关键字相当于一个暂停标记,函数执行到yield
时将暂停执行,而调用next
方法会恢复函数的执行,yield
有以下特点:
- 只能在
生成器函数
的顶层作用域使用 - 函数执行到
yield
时,将暂停函数执行,并将紧跟yield
关键字的表达式的值返回给next
执行结果的value
属性
与return的异同:
-
相同点
都会返回紧跟在后面表达式的值
-
不同点
代码遇到
yield
会暂停,并在下次执行时从该位置继续执行代码遇到
return
就执行结束了.
由于yield
关键字的存在,生成器函数
可以返回多个值,就像一个个生成值一样,这也是生成器
名字的由来,正是因为该关键字,让生成器函数可以在函数执行过程中返回多个值出来
yield*
在生成器内部调用另一个生成器时,一般做法是通过for of
迭代另一个生成器:
javascript
function* generatorFn(){
yeild 1 + 2;
for(let key of otherGeneratorFn()){ // 手动遍历
yield key
}
return 8
}
这样无疑很繁琐,不过es6提供了yield*
来解决这个问题:
javascript
function* generatorFn(){
yeild 1 + 2;
yield* otherGeneratorFn()
return 8
}
实际上**yield*
可以迭代任何可迭代对象**,这样边可以轻松实现生成器的递归操作
next方法的参数
yield
本身没有返回值,或者说他的返回值是undefined
,如下例所示:
vbnet
function* generatorFn(){
const a = yeild 1 + 2;
return a
}
const generator = generatorFn()
generator.next() // 执行到第一个yield暂停函数的执行,并返回yield后表达式的值: {value: 3,done: false}
generator.next() // 继续上个yeild 将yeild本身的值赋值给a,执行到return,函数结束返回a的值: {value: undefined,done: true}
next
方法可以携带一个参数,这个参数会作为上一次让函数暂停的 yield
的返回值:
vbnet
function* generatorFn(){
const a = yeild 1 + 2;
return a
}
const generator = generatorFn()
generator.next(2) // 虽然传递参数了,依然输出{value: 3,done: false},因为参数只传给上次让函数暂停的yield,这是第一次执行,因此上次的yield是不存在的
generator.next(2) // 输出{value: 2,done:true},next的参数将传递给上次暂停的yield,因此代码`yeild 1+2`中yield的值为2,赋值给a后由return返回
与迭代器接口的关系
上文说过: 生成器函数执行时返回一个生成器对象,生成器对象可以当作迭代器对象使用,所以说生成器函数其实就是一个迭代器工厂函数,因此它可以直接赋值给迭代器接口[Symbol.iterator]
,作为迭代器工厂函数使用,实际上由于yield
的存在,使用生成器作为迭代器工厂函数更加方便,
还记得上文中实现自定义迭代器的例子吗?现在使用生成器重写一下用以比较:
javascript
// 将一个object变成可迭代对象
function addIterator(obj){
const objCopy = _.deepCopy(obj)
Object.defineProperty(objCopy,Symbol.iterator,{
enumerable: false,
writable: false,
configurable: true,
value: function* genertorfn(){
const keys = Object.keys(objCopy);
for(let key of keys){
yield objCopy[key]
}
}
})
return objCopy
}
let obj = { a: 1, b: 2 };
obj = addIterator(obj);
for (let value of obj) {
console.log(value); // 1 2
}
可以看到没有嵌套,没有闭包就可以实现,并且可读性更强
提前终止生成器
调用生成器函数返回一个生成器对象,该对象也有关闭的概念,如果进入关闭状态就无法恢复了,后续调用next
返回done
为true
,提供的任何返回值都不会被存储或传递.
迭代器对象有的是可以被关闭的,有的不可以,而生成器不同,可以强制进入关闭状态,他是如何做的呢? 主要通过它的两个方法return()
或throw()
return
迭代器对象的return
方法是可选的,而每个生成器都自带return
方法,调用该方法就可以使生成器函数进入关闭状态:
javascript
function* generatorFn(){
yield 1;
yield 2;
yield 3;
return 4
}
const generator = generatorFn()
generator.next() // {value: 1,done: false}
generator.return() // {value: undefined,done: true} 强制关闭生成器
generator.next() // {value: undefined,done: true} 关闭后将不再迭代
return
方法还可以传递一个参数,该参数最终传递给迭代结果的value
属性,上述例子稍微修改一下:
yaml
generator.next() // {value: 1,done: false}
generator.return(4) // {value: 4,done: true} 强制关闭生成器,value为return传递的值
throw
调用该方法会在暂停的时候将一个提供的错误注入到生成器对象中,如果错误未被生成器内部处理,生成器就会关闭
javascript
function* gen() {
while (true) {
yield 42;
}
}
// 外部处理错误,即使捕获了也会关闭生成器
const g = gen();
console.log("g.next();:", g.next());// { value: 42, done: false }
try {
g.throw(new Error("出现了些问题"));
} catch (e) {
console.log("捕获到外部错误"); // 捕获到外部错误
}
console.log("g.next();:", g.next()); // { value: undefined, done: true }
//内部捕获错误,生成器不会被关闭
function* gen() {
while (true) {
try {
yield 42;
} catch (e) {
console.log("捕获到错误!");
}
}
}
const g = gen();
console.log("g.next();:", g.next());// { value: 42, done: false }
g.throw(new Error("出现了些问题")); // "捕获到错误!"
console.log("g.next();:", g.next());// { value: 42, done: false }
彻底理解next,return,throw
这三个生成器属性有一个共同点: 都可以看作是用来替换yield
和后面的表达式的
- next是将
yield
表达式替换为一个值
vbnet
// next:
function* generatorFn(){
let a = yield 1;
let b = yield 2;
yield 3;
return 4
}
const generator = generatorFn()
generator.next()
generator.next('哈')
//上面的next相当于:
//将 let a = yield 1中yield替换为 let a = '哈'
- return是将
yield
替换为return
语句
vbnet
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
- throw是将
yield
替换为throw
语句
vbnet
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
强烈推荐:自己感觉比javascript高级程序设计描述的更易懂且全面: 阮一峰Grnerator