送代器Iterator是ES6提出的一种接口机制。它的目的主要在于为所有部署了Iterator接口的数据结构提供统一的访问机制,即按一定次序执行遍历操作。并且ES6也提出了针对Iterator遍历操作的专属遍历命令的标准,即for of循环
一个数据结构只要具有Symbol.iterator属性,就可以认为是"可迭代的"(iterable)
Symbol.iterator属性本身是一个函数, 执行这个函数,就会返回一个迭代器Iterator,当使用for…of循环遍历某种数据结构时,该循环会自动去寻找Iterator接口,也就是对Symbol.iterator函数的返回的迭代器Iterator进行遍历。
Iterator 在遍历时每次会调用Iterator的next方法,按顺序依次指向数据机构的每个成员。next方法的返回值返回数据结构的当前成员的信息,是一个包含value和done两个属性的对象,其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
数组,字符串以及ES6新增的Map和Set结构默认拥有Iterator接口,对象默认没有
let arr = ['a','b','c'];
let iter = arr[symbol.iterator]():
for(const item of arr){
console.log(item)//'a' 'b' 'c'
}
//或者(注意next遍历方式和roror遍历方式不要同时存在)
iter.next() // {value: 'a', done: false }
iter.next () // {value: 'b', done: false }
iter.next() // {value: 'c', done: false }
iter.next() // {value: undefined, done: false }
注意
上述通过for of遍历当前数据结构和通过next调用当前数据结构的Iterator的两种方式如果同时存在,这两种方式会共享当前数据结构的遍历状态。比如for of若己遍历完毕,再使用next方式遍历,则会返回{ valuet undefined, done: true}
1:通过class方式给实例对象添加Iterator接口
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
}
return {done: true, value: undefined};
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log(value); // 0, 1, 2
}
解析:
range(0,3)
返回class构造函数的一个实例对象,对其进行for of循环时,去找它的[symbol.iterator]属性(即iterator)。由于class定义的方法都是添加在当前构造函数的原型链上的,所以该实例对象存在iterator属性。
又因为该方法返回了this即当前实例对象的引用。所以调用Iterator的next方法时,调用的也就是clasa中定义的next方法(即访问的是实例对象的原型链上的next方法)
这个class实现了Iterator接口,但是有一点小问题,就是它的每个实例只能被迭代一次
优化:同一个实例,可以多次进行选代:
class RangeIterator{
constructor(start, stop){
this.start = start;
this.stop= stop;
}
[Symbol.iterator](){
let start = this.start
let stop = this.stop
return {
next(){
var value = start
if(value<stop){
start++
return {done:false,value:value}
}
return {done:true.value:undefined}
}
}
}
}
const range = new RangeIterator(0,3);
for (var value of range) {
console.log(value); // 0, 1, 2
}
for (var value of range) {
console.log(value); // 0, 1, 2
}
2:为object添加Iterator接口
let obj = {
//用于遍历对象的key
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
}
return { value: undefined, done: true };
}
};
}
};
上述方法定义Iterator接口有些许麻烦和繁琐,而ES6提出的generator函数(即生成器)方式则是Iterator接口最简单实现的方式
一般来讲,函数一旦执行就会运行到结束,期间不会有其他代码能打断它。而生成器提供一种脱离这种模式的看似同步的异步流程控制方式
let x = 1
function *foo(){
x++
yield //暂停
console.log('x',x)
}
function bar(){
x++
}
const it = foo()
//启动
it.next ()
x; //2
bar()
x;//3
it.next() //x,3
代码解析:
1:调用foo返回一个迭代器it来控制这个生成器,此时生成器并没有开始执行
2:it执行next()启动了生成器*foo,并执行了第一行x++。当生成器执行到yield时暂停,此时这次 next执行结束
3:打印x值为1+1=2,然后再执行bar,2+1=3。
4:最后又执行一次next则从yield暂停处恢复执行,执行了console.log,打印了x的值3
function *foo(x){
const y = x* (yield)
return y
}
const it = foo(6)
//开始运行
const res1 = it.next()
res1: //{value:undefined,done:false}
const res2 = it.next(7)
res2: //{value:42,done:false}
代码解析:
1:将6作为generator函数即生成器foo的参数x的值传入
2:foo(6)返回一个选代器iterator传给变量it
3:it.next()执行到foo中的第一行代码yield处暂停。注意,此时第一行代码还没执行完,y还没有被赋值。只是在执行-等号右边表达式的yield处卡住了
4:接下来执行it.next(7),此时7将赋值给当前暂停的yeild关键字。然后执行x*yeild
即6*7=42并赋值给y。接下来碰见retun,则return值将作为当前next的返回值的value
问题:yield到底是什么? next返回值到底的什么决定?为什么next调用的次数和yield执行的次数不匹配?
先让我们继续看下一个例子:
function *foo(){
const x = yield 2
z++
const y = yield (x*z)
console.log(x,y,z)
}
var z=1
var it1 = foo()
var it2 = foo()
var val1 = it1.next().value //2---yiela2
var va12 = it2.next().value //2----yield2
val1 = it1.next(val2*10).value //40----x:20,z:2
va12 = it2.next(val1*5).value //600----x:200,2:3
it1.next(va12 / 2) //y:300 20 300 3
it2.next(val1 / 4) //y:10 200 10 3
代码解析:
1:创建两个迭代器it1,it2,均来自于生成器foo
2:it1,it2都分别执行第一个next卡在了第一行的yield表达式,其返回的value值为第一个yield后面跟着的值,即2
3:it1再次调用next并把val2 * 10
即2*10=20
传参进去作为上一个yield表达式整体的值,即x=20。然后z++即z=2,此时it1卡在了第三行的yield表达式。当前next返回的value值为该yield后面跟的值,即 x*z
即20*2=40
。并赋值给val1
4:it2同样再次调用next并把val1*5
即40*5=200
传参进去作为上一个yield表达式整体的值,即x=200。然后z++即z-3(z是全局变量),此时it2同样卡在了第三行的yield表达式,当前next返回的value值为该yield后面跟的值,即 x*2
即200*3=600
,并赋值给val2
5:it1第三次调用next,将val2 / 2
即600 / 2=300
传参进去作为上一个yield表达式整体的值,即y=300。最终打印x=20,y=300,z=3
6:it2第三次调用next,将val1 / 4
即40 / 4=10
传参进去作为上一个yield表达式整体的值,即y=10。最终打印x-200.y-10,z-3
结论:
{value:undefined,done:true}
。即使return的后续代码还存在yield,则依然被忽略for of
来遄历(本质也是在调用next)。则碰到return返回值不会遍历出来,只会遍历出所有yield后面跟的值it.throw()
在生成器外部抛出的错误,如果在生成器内部没有进行错误捕捉(没有定义try/catch),生成器会被关闭。ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同于
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
结论:
异步流程的控制是生成器的另一大特点和适用场景。
而在ES6中,异步流程控制的最优解来自于Promise与生成器的组合:
//异步请求的方法
function foo(x,y){
//设request为一个异步请求第三方库的API,类似axio//request调用返回值为promise对象
return request('http://xxx.xxx'+x+y)
}
//生成器
function *main(){
try{
const data - yield foo("11 ,"31')
console,log(data)
}catch (e){
console.log(e)
}
}
const it = main()
const p = it.next().value
p.then(data=>{
it.next(data)
},(e)=>{
it.throw(e)
})
代码解析:
核心点:
获得Promise和生成器最有效的最自然的方法就是yield出来一个promise,然后通过这个promise控制生成器的迭代器的执行