前言
什么是迭代器和生成器,其实这里有很多概念是大家经常不理解和忽略的地方。
迭代器其实就是一种类似于Promise+
规范,迭代器是一个复合迭代器规范的函数,是不是有点拗口。迭代器函数返回一个含有next
方法的对象,next
方法返回一个包含 value done
属性的对象。
生成器就是 function * ( ){}
格式的,加上yield
进行暂停。
1.迭代
1.1 什么是迭代
你给我翻译翻译什么叫做迭代,迭代就是按照顺序遍历执行的具有终止条件的一段程序。记住三个特点:有序、循环遍历以及终止条件。
- 有序:迭代会在一个有序集合上进行。“有序”指的是集合中所有项均可按照定义的顺序进行遍历,其中开始项和结束项是明确定义的。(如数组就是有序集合)
- 循环遍历:每次循环都是在下一次迭代开始之前完成。
- 终止条件:循环遍历的次数。(没有终止条件的循环是死循环,那不完犊子了吗)
const colors = ["red","green","blue"];
colors.forEach(item=>console.log(item));
//"red"
//"green"
//"blue"
复制代码
1.2 数组迭代的缺陷
- 迭代前需要事先知道如何使用数据结构。
- 不适用于除数组之外的隐式顺序的数据结构。
1.3 ES5中的数组迭代
- forEach()
- map()
- some()
- every()
- filter()
2.迭代器
2.1 迭代器和可迭代对象
迭代器指的是被设计来专门迭代的对象,带有特定的接口。即:按需创建的一次性对象。
可迭代对象指的是实现了正式的Iterbale接口,并且可以通过迭代器Iterator消费的结构。说人话,就是可以遵循某些规则从中取出数据的对象。
这些规则就是:
- 主对象/类应该存储一些数据。
- 主对象/类必须具有全局的
Symbol
,且这个属性必须使用特殊的Symbol.iterator
作为键。 Symbol.iterator
方法必须一个新的迭代器对象──“默认迭代器”。- 迭代器对象必须有一个next()方法
- 每次成功调用next(),都必须返回迭代结果对象,其中包含迭代器返回的下一个值
- 若不调用next(),则无法知道迭代器的当前位置
每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的API。
2.2 可迭代协议
可迭代协议具有两种能力:
- 支持迭代的自我意识。用于解决迭代前需要事先知道数据结构类型的缺陷。
- 创建实现可迭代接口的对象的能力。用于解决迭代不适用于除数组之外的隐式顺序的数据结构。
其实,就是通过在对象或对象原型链上暴露“默认迭代器”属性来获取迭代行为,这个属性使用特殊的Symbol.iterator
作为键,调用迭代器工厂函数返回一个新的迭代器。
内置可迭代对象接口的类型
- String
- Array
- Map
- Set
- argument
- NodeList等DOM集合对象
接收可迭代对象的语法
- for-of循环
- 数组解构
- 扩展运算符
- yield* 操作符,在生成器中使用
- Array.from()
- 创建集合new Map()
- 创建映射new Set()
- Promise.all()接收由Promise组成的可迭代对象
- Promise.race()接收由Promise组成的可迭代对象
这些语法结构会在后台调用提供的可迭代对象的工厂函数,从而创建一个迭代器。
自定义可迭代对象
如果对象原型链上的父类也实现了Iterable接口,那么这个对象也就实现了可迭代接口。
class SonArray extends Array{}
const sonArray = new SonArray("zhangsan","lisi","waner");
for(let item in sonArray){
console.log(item);
}
// "zhangsan"
// "lisi"
// "wenger"
复制代码
2.3 迭代器协议
我们知道,迭代器是一种一次性使用的对象,用于迭代与其相关的可迭代对象。
一句话说明就是:只有实现next()方法的对象才能成为迭代器。
next()调用成功后返回两个值,分别是:done和value。done表示的就是本次next调用成功,可以进行下一次next()的调用;value表示的是包含下一次调用next()的值。(done为false时,value为对应的值;done为true时,value为undefined)
举例子:
//可迭代对象
const colors = ["red","green","blue"];
//迭代器工厂函数,获取迭代器
let iter = colors[Symbol.iterator]();
//执行迭代
console.log(iter.next());//{done:false,value:"red"};
console.log(iter.next());//{done:false,value:"green"};
console.log(iter.next());//{done:false,value:"blue"};
复制代码
我们看到,这里通过创建迭代器并且调用了next()方法进行顺序迭代数组,直到遍历完所有的值(即:done为true时停止迭代)。
迭代器的特点:
- 每个迭代器都表示对可迭代对象的一次性有序遍历。
- 迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。
迭代器的概念比较模糊,有时候指的是迭代,有的时候指的是接口,还可以指迭代器类型。那么迭代器的真面目是:
class Colors{
// 迭代器
[Symbol.iterator] () {
return {
// next() 函数
next () {
// 返回 IteratorReault
return { done: false, value: 'red' };
}
}
}
}
//创建一个实例对象
const colors = new Colors();
//打印实现了迭代器接口的对象
console.log(colors[Symbol.iterator]()); // { next: colors () {} }
//
console.log(color[Symbol.iterator]().next()); // {done: false, value: "red"}
复制代码
2.4 自定义迭代器
任何实现Iterator接口的对象都可以作为迭代器使用,为了能够将可迭代对象迭代多次,需要迭代器对象上创建对个迭代器,每一个迭代器对应一个新计数器,再将计数器变量放到闭包中,通过闭包返回迭代器。
例如:
class Counter {
constructor (limit) {
this.limit = limit;
}
[Symbol.iterator] () {
let count = 1,
limit = this.limit;
retrun {
next () {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
}
}
}
}
let counter = new Counter(3);
for(let i of counter){
console.log(i);
}
// 1
// 2
// 3
复制代码
这样,每个以这种方式创建的迭代器均实现了可迭代接口。
2.5 提前终止迭代器
迭代器的可选return()可以用于指定让迭代器提前终止执行迭代。
- for-of 循环通过 break\continue\return\throw 等提前退出
- 解构赋值并未消费所有值
例如:
class Counter {
constructor (limit) {
this.limit = limit;
}
[Symbol.iterator] () {
let count = 1,
limit = this.limit;
return {
next () {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
},
// 加入 return
return () {
console.log('迭代器关闭前执行操作');
return { done: true };
}
}
}
}
let counter = new Counter (5);
for (let i of counter) {
if (i > 2) break;
console.log(i);
}
// 1
// 2
// 迭代器关闭前执行操作
复制代码
注意:
- 没有设定
return()
关闭迭代器或迭代器不能关闭的情况下,迭代退出后还可从上次离开的地方继续迭代 - 设定了
return()
的迭代器不会强制进入关闭状态,但return()
会被调用
2.生成器
前面已经将迭代器的基本概念和特性已经整理清楚,接下来我们开始讲解生成器。
2.1 什么是生成器
生成器(Generator
)函数是es6提供的异步编程解决方案,语法和传统函数不同。生成器函数是一个状态机,封装了多个内部状态,通过yield
暂停执行,通过next()
进行下一次迭代。
简而言之,生成器就是用来控制迭代器的函数,可以随时暂停,随时恢复。
生成器也可以看作是一个值的生产者,通过迭代器接口的next()
可以调用一次取得一次值。
如何定义一个生成器,很简单在函数定义前面加上* 即可:
//函数声明式生成器
function *fun(){}
//函数表达式生成器
let fun = function*(){}
//字面量式生成器
let fun = {
* foo(){}
}
//等等
复制代码
2.2 生成器的特点
- 标识生成器函数的星号* 不受两边空格影响
- 箭头函数不能用于定义生成器函数(着重注意,所以箭头函数没有arguments属性)
- 调用生成器函数会产生一个生成器对象
- 生成器对象初始状态处于暂停执行(
suspended
)状态 - 生成器对象内置
Iterator
接口,具有next()
方法 - 函数体内部使用yield表达式,定义不同的内部状态
- 生成器函数时分段执行的,调用
next()
方法函数内部逻辑开始执行 - 函数体为空的生成器函数中间不会停留,调用一次
next()
就到达done:true
状态 - 生成器对象只会在初次调用
next()
方法后才会执行 - 生成器对象实现了可迭代接口,默认迭代器是自引用的
例如:
let generatorFn = function* () {
console.log('执行');
return 'generator'
};
const g = generatorFn();
console.log(g); // generatiorFn {<suspended>}
console.log(g.next);// ƒ next() { [native code] }
console.log(g.next());
// 执行
// {value: "generator", done: true}
console.log(generatorFn);
/* ƒ* () {
console.log('执行');
return 'generator'
}*/
console.log(generatorFn()[Symbol.iterator]); // ƒ [Symbol.iterator]() { [native code] }
console.log(generatorFn()); // generatorFn {<suspended>}
console.log(generatorFn()[Symbol.iterator]()); // generatorFn {<suspended>}
复制代码
我们看下Generator 函数是如何实现分段执行的。
function *Gen(){
yield "wenbo";
yield "bowen";
yield "zhaoshun";
return "ending";
}
const gen = Gen();
gen.next();//{done:false,value:"wenbo"}
gen.next();//{done:false,value:"bowen"}
gen.next();//{done:false,value:"zhaoshun"}
gen.next();//{done:true,value:undefined}
复制代码
我们看到前三次调用next()
都会遇到yield
,此时函数会暂停执行,分别返回一个value
和done
。判断done
的值不是true
,说明迭代还没停止,此时继续遍历。
当执行第四次next()
时遇到return
(如果没有return
,表示直到函数执行结束)。next()
方法返回的对象的value
属性,紧跟在return
语句后面表达式的值,此时done
的值为true
,表示遍历结束。
yield中断执行
yield
关键字可以让生成器停止和开始执行。生成器函数初次执行时,当遇到yield
会暂停执行,函数作用域的状态会被保留。当再进行调用next()方法时,生成器函数只有通过在生成器对象调用next()方法来恢复执行。
生成器对象作为可迭代对象
在生成器对象可以不同显式调用next()
,可以把迭代器对象当成可迭代对象。
function *Gen(){
yield "wenbo";
yield "bowen";
yield "zhaoshun";
}
for(const item of Gen()){
console.log(item);
}
// "wenbo"
// "bowen"
// "zhaoshun"
复制代码
使用yield实现输入输出
yield
既可以作为函数的中间返回语句使用,也可以作为函数的中间参数使用。第一次调用next()
的传入的值不会被使用,是为了执行生成器函数。
function *Gen(init){
console.log(init);
console.log(yield);
console.log(yield);
}
const gen = Gen("fun");
gen.next("wenbo");//wenbo
gen.next("bowen");//bowen
gen.next("zhaoshun");//zhaoshun
复制代码
产生可迭代对象 可以使用星号* 增强yield的行为,能够让那迭代一个可迭代对象,从而每次产出一个值。
因为yield* 实际上只是将一个可迭代对象序列化为一串单独产出的值,所以放在循环中无异。
function *Gen(){
for(const x of [1,2,3]){
yield x;
}
}
//等价于
function *Gen(){
yield* [1,2,3];
}
复制代码
在生成器对象中实现了Iterator接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器各位适合作为默认迭代器。由于 Generator
函数就是遍历器生成函数,因此可以把Generator
赋值给对象的Symbol.iterator
属性,从而使得该对象具有 Iterator
接口。
let obj = { username: 'wenbo', age: 19 }
obj[Symbol.iterator] = function* myTest() {
yield 1;
yield 2;
yield 3;
};
for (let i of obj) {
console.log(i) // 1 2 3
}
复制代码
提前终止生成器
一个实现Iterator
接口的对象一定有next()
方法,还有一个可选的return()
方法用于提前终止迭代器。return()
和throw()
方法均可以实现强制生成器进入关闭状态。
- return()方法会强制生成器进入关闭状态。提供给return()方法的值,就是终止迭代器对象的值
- throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未处理,生成器就会被关闭。
function* Gen(){
for(const x of ["red","green","blue"]){
yield x;
}
}
const gen = Gen();
console.log(gen.next());//{done:false,value:"red"}
console.log(gen.return("yellow"));//{done:false,value:"yellow"}
console.log(gen.next());//{done:true,value:undefined}
console.log(gen.next());//{done:true,value:undefined}
function *Fun(){
for(const x of ["red","green","blue"]){
yield x;
}
}
const fun = Fun();
console.log(fun);//Fun {<suspended>}
try{
fun.throw("yellow");
}catch(e){
console.log(e);//yellow
}
console.log(fun);//Fun {<closed>}
复制代码
应用场景
协程场景
function schoolTask(goSchool,endSchool){
let goSchoolIterator=goSchool();
let endSchoolIterator=endSchool();
console.log(goSchoolIterator.next().value);
console.log(endSchoolIterator.next().value);
console.log(endSchoolIterator.next().value);
console.log(endSchoolIterator.next().value);
console.log(goSchoolIterator.next().value);
console.log(endSchoolkIterator.next().value);
}
function *goSchool(){
yield "我上了小学";
yield "我上了中学";
yield "我上了大学";
}
function *endSchool(){
yield "我小学毕业了";
yield "我中学毕业了";
yield "我大学毕业了";
}
schoolTask(goSchool,endSchool);
/*
我上了小学
我小学毕业了
我上了中学
我中学毕业了
我上了大学
我大学毕业了
*/
复制代码
异步操作同步化
ajax
异步操作同步化的核心就是:Generator
函数里的yield
关键字,它可以中断执行!
其实就是:
当迭代器it
第一次调用了next()
方法,启动生成器,ajax
就会发起请求,当ajax
完成时,通过回调函数,它就会再次启动next()
方法,将接收到的数据,赋值给上一次yield
表达式,于是变量result
就收到了ajax
发送过来的请求,最后把请求到的数据进行格式化并输出。
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();
复制代码
小结
- 迭代器:一个可以由任意对象实现的接口,支持连续获取对象产出的一个只。任何实现
Iterable
接口的对象都有一个Symbol.iterator
属性,这个属性引用默认迭代器。默认迭代器就系那个一个迭代器工厂,调用之后就会产生一个实现Iterator
接口的对象。 - 生成器:一种调用之后会返回一个生成器对象的特殊函数。生成器对象实现了
Iterable
接口,因此可用于任何消费可迭代对象的地方。生成器支持yield
关键字,能够暂停执行生成器函数;使用yield
可以通过next()
方法接受输入和产生输出。
参考文章
《JavaScript高级程序设计(第四版)》
写在最后
感谢大家的阅读,我将继续和大家分享更多优秀的文章,期待大家的关注,咨询交流请联系微信:codepanda2020。
最后感谢大家对于本公众号的大力支持,不忘初心,继续热爱,也希望大家能够点亮我的公众号的【大拇指】、【小方块】、【小星星】,嘿嘿,感谢。