迭代器生成器
Promise来实现异步也会存在一些问题,比如代码量增多,不易理解。另外一种异步操作的方法—-生成器。这是ES6新增的方法,在讲它之前,咱们还得理解另外一个东西:迭代器。
迭代器(Iterator)
迭代
从一个数据集合中按照一定的顺序,不断的取出数据的过程
迭代和遍历区别
迭代强调是依次取出,不能确定取出的有多少,也不能保证把数据全部取完
遍历要知道数据的长度,不断的全部取出数据
迭代器
对迭代过程的封装,通常为对象,不同的语言中,表现出来的迭代形式不一样
迭代器有得到下一个数据的能力
判断是否有后续数据的能力
迭代模式
一种设计模式,用于同一迭代的过程,并且规范迭代器的规格
js中的迭代器
js的规定,如果一个对象有next方法,并且返回一个对象,就认为这个对象为迭代器
迭代器
const obj = {
next(){ //用于得到下一个数据
return {
//value : xxx数据
//done : xxx 判断是否有后续数据,一般为boolean
}
}
}
迭代器是一种接口,也可以说是一种规范。它提供了一种统一的遍历数据的方法。我们都知道数组、集合、对象都有自己的循环遍历方法。
比如数组的循环:
let ary = [1,2,3,4,5,6,7,8,9,10];
//for循环
for(let i = 0; i < ary.length; i++){
console.log(ary[i]);
}
// forEach循环
ary.forEach(function(ele){
console.log(ele);
});
//for-in循环
for(let i in ary){
console.log(ary[i]);
}
//for-of循环
for(let ele of ary){
console.log(ele);
}
集合的循环:
let list = new Set([1,2,3,4,5,6,7,8,9,10]);
for(let ele of list){
console.1og(ele);
}
对象的循环:
let obj = {
name :'Jack',
age : 25,
gender:'男',
intro : function(){
console.log('my name is ' +this.name);
}
}
for(let attr in obj){
console.log(attr);
}
从以上的代码可以看到,数组可以用for、forEach、for-in以及for-of来遍历。集合能用for-of。对象能用for-in。也就是说,以上数据类型的遍历方式都各有不同,那么有没有统一的方式遍历这些数据呢?这就是迭代器存在的意义。它可以提供统一的遍历数据的方式,只要在想要遍历的数据结构中添加一个支持迭代器的属性即可。这个属性写法是这样的:
const obj = {
[Symbol.iterator]:function(){ }
}
[Symbol.iterator]属性名是固定的写法,只要拥有了该属性的对象,就能够用迭代器的方式进行遍历。
迭代器的遍历方法是首先获得一个迭代器的指针,初始时该指针指向第一条数据之前。接着通过调用next方法,改变指针的指向,让其指向下一条数据。每一次的next都会返回一个对象,该对象有两个属性。
其中value代表想要获取的数据,done是个布尔值,false表示当前指针指向的数据有值。true表示遍历已经结束。
let ary = [1,2,3];
let it = ary[Symbol.iterator](); //获取数组中的迭代器
console.1og(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.1og(it.next()); // { value: 3, done: false }
console.1og(it.next()); // { value: undefined, done: true }
数组是支持迭代器遍历的,所以可以直接获取其中的迭代器。集合也是一样
let list = new Set([1,2,3]);
let it = list.entries(); //获取set集合中的迭代器:
console.log(it.next()); // { value: [ 1, 1 ],done: false }
console.log(it.next()); // { value: [ 2, 2 ],done: false }
console.log(it.next()); // { value: [ 3, 3 ], done: false }
console.log(it.next()); // { value: undefined, done: true }
set集合中每次遍历出来的值是一个数组,里面的第一和第二个元素都是一样的
由于数组和集合都支持迭代器,所以它们都可以用同一种方式来遍历。
es6中提供了一种新的循环方法叫做for-of。
它实际上就是使用迭代器来进行遍历,换句话说只有支持了迭代器的数据结构才能使用for-of循环。
在JS中,默认支持迭代器的结构有:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
for of 循环 只能循环一个可以迭代的对象,数组,类数组改成了可迭代的对象
自己定义的对象是不能迭代的,但是可以通过增加符号属性变成可迭代的对象
在es6中,如果对象具有知名符号的属性 Symbol.iterator,则表示对象可以迭代
let obj = {
name : ' Jack',
age : 25,
gender : '男',
intro : function(){
console.1og('my name is ' +this . name);
},
[Symbol.iterator]: function(){
let i = 0;
let keys = object.keys(this); //获取当前对象的所有属性并形成一个数组
return {
next: function(){
return {
value:keys[i++], //外部每次执行next都能得到数组中的第i个元素
done:i > keys.length //如果数组的数据已经遍历完则返回true
}
}
}
}
}
for(let attr of obj){
console.log(attr);
}
生成器(Generator)
什么是生成器?
生成器就是通过构造函数Generator创建出来的对象,生成器既是一个迭代器,同时又是一个可迭代的对象
创建
只需要把函数变成Generator函数
function *say(){}
const say = function*(){}
注意: 这个"*"只能写在function关键字的后面
async和* 不能同时加在一个函数上,因为同时起了函数修饰符的作用
生成器函数和普通函数并不只是一个“*”号的区别。普通函数在调用后,必然开始执行该函数,直到函数执行完或遇到return为止。中途是不可能暂停的。
但是生成器函数则不一样,它可以通过yield关键字将函数的执行挂起,或者理解成暂停。它的外部在通过调用next方法,让函数继续执行,直到遇到下一个yield,或函数执行完毕。
function* say(){
yield "开始";
yield "执行中";
yield "结束";
}
let it = say(); // 调用say方法,得到一个迭代器
console.log(it.next()); // { value: '开始', done: false }
console.log(it.next()); // { value: '执行中', done: false }
console.log(it.next()); // { value: '结束', done: false }
console.log(it.next()); // { value: undefined, done: true }
调用say函数,这句和普通函数的调用没什么区别。但是此时say函数并没有执行,而是返回了一个该生成器的迭代器对象。接下来就和之前一样,执行next方法,say函数执行,当遇到yield时,函数被挂起,并返回一个对象。对象中包含value属性,它的值是yield后面跟着的数据。并且done的值为false。再次执行next,函数又被激活,并继续往下执行,直到遇到下一个yield。当所有的yield都执行完了,再次调用next时得到的value就是undefined,done的值为true。
理解了迭代器,生成器也就很好理解了,它的yield,其实就是next方法执行后挂起的地方,并得到你返回的数据。
那么这个生成器有什么用呢?
它的yield关键字可以将执行的代码挂起,外部通过next方法让它继续运行。这和异步操作的原理非常类似,把一个操作分为两部分,先执行一部分,然后再执行另外一部分。所以生成器可以处理和异步相关的操作。我们知道,异步操作主要是依靠回调函数实现。但是纯回调函数的方式去处理同步效果会带来“回调地域“的问题.Promise可以解决这个问题。但是Promise写起来代码比较复杂,不易理解。而生成器又提供了一种解决方案。
function* delay(){
yield new Promise((resolve,reject) => {setTimeout(()=>{resolve()},2000)})
console.log("go on");
}
let it = delay();
it.next().value.then(()=>{
it.next();
});
这个例子实现了等待2秒钟后,打印字符串”go on”。
在delay这个生成器中,yield后面跟了一个Promise对象。这样,当外部调用next时就能得到这个Promise对象。然后调用它的then函数,等待2秒钟后Promise中会调用resolve方法,接着then中的回调函数被调用。也就是说,此时指定的等待时间已到。然后在then的回调函数中继续调用生成器的next方法,那么生成器中的代码就会继续往下执行,最后输出字符串”go on”。
例子中时间函数外面为什么要包裹一个Promise对象呢?这是因为时间函数本身就是一个异步方法,给它包裹一个Promise对象后,外部就可以通过then方法来处理异步操作完成后的动作。这样,在生成器中,就可以像写同步代码一样来实现异步操作。
注意的点:
1.生成器有返回值,出现在最后一次的done为true的value中
2.调用生成的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值
3.第一次去调用函数的时候,传递的参数是没有任何含义的
参考 : https://zhuanlan.zhihu.com/p/74637286
其他的API
- return方法 调用以后,可以提前结束生成器函数,整个的迭代过程提前结束
function *test(){
yield 1;
yield 2;
yield 3;
return 4;
}
const senerator = test();
- throw 可以在生成器产生一个错误,抛出到对应的行
- 在函数内部是调用别的生成器的,在生成器函数内部去调用别的生成器函数,一定要加上*
//在函数内部是调用别的生成器
function *fun1(){
yield "a";
yield "b";
}
function* fun2(){
// fun1()
// yield fun1()
yield *fun1() //在生成器函数内部去调用别的生成器函数,一定要加上*
yield 1;
yield 2;
yield 3;
}
const generator = fun2();
console.log(generator)