JavaScript 生成器(Generator函数)和迭代器(iterator) 使用通俗讲解

1.迭代器

送代器Iterator是ES6提出的一种接口机制。它的目的主要在于为所有部署了Iterator接口的数据结构提供统一的访问机制,即按一定次序执行遍历操作。并且ES6也提出了针对Iterator遍历操作的专属遍历命令的标准,即for of循环

1.1默认Itearator接口

一个数据结构只要具有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.2 自定义Icerator

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接口最简单实现的方式

2.生成器(generator函数)

2.1:基本模式

一般来讲,函数一旦执行就会运行到结束,期间不会有其他代码能打断它。而生成器提供一种脱离这种模式的看似同步的异步流程控制方式

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

2.2 消息传递

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 * 102*10=20传参进去作为上一个yield表达式整体的值,即x=20。然后z++即z=2,此时it1卡在了第三行的yield表达式。当前next返回的value值为该yield后面跟的值,即 x*z20*2=40。并赋值给val1

4:it2同样再次调用next并把val1*540*5=200传参进去作为上一个yield表达式整体的值,即x=200。然后z++即z-3(z是全局变量),此时it2同样卡在了第三行的yield表达式,当前next返回的value值为该yield后面跟的值,即 x*2200*3=600,并赋值给val2

5:it1第三次调用next,将val2 / 2600 / 2=300传参进去作为上一个yield表达式整体的值,即y=300。最终打印x=20,y=300,z=3

6:it2第三次调用next,将val1 / 440 / 4=10 传参进去作为上一个yield表达式整体的值,即y=10。最终打印x-200.y-10,z-3

结论:

  • 每当迭代器调用一次next方法,就一定会执行到yield处暂停或者return处停止。此时当前next方法调用后的返回值对象中的value值
    等于yield后面跟的那个值或者return值(存在return,则next返回的done值为true,value为return值)。若yield或return后面没有跟值则当前next方法的value值为undefined
  • 而yield表达式整体本身(如yield 2整体)也代表一个值。它的值永远由下一个next方法调用时的传参来决定。"下一个next方法“是什么意思呢?比如第一个next()总是启动生成器并运行到第一个yield处。但此时即使给第一个next传参也不会给整个yield表达式赋值,因为它永远都会卡在yieId表达式,而不是“完成"yleld表达式,只有执行到第2个next方法时才可以去”完成”第1个yield表达式。以此类推,第3个next方法去“完成”第2个yield表达式…所以next调用的次数一般都比yield表达式完成的次数多一次
  • 同一个generator函数可以创建多个迭代器,其函数内部的变量值各自独立,互不影响,但可共享全局变量。

2.3其他细节

  • 由于第一个next执行时并不会去“完成”第一个yiela表达式,而是卡在第一个yield表达式。而yield表达式整体的值又由next调用时的参数决定,所以第一次调用next传递的参数都会被丢弃
  • 调用next时若碰到return或已执行完最后一个yield,则此时next返回值的value为return值(无return则为undefined),done为true。并且之后无论调用多少次next返回值都是{value:undefined,done:true}即使return的后续代码还存在yield,则依然被忽略
  • 如果不采用next去消费生成器的值,而是通过for of来遄历(本质也是在调用next)。则碰到return返回值不会遍历出来,只会遍历出所有yield后面跟的值

2.4 throw

  • 通过it.throw()在生成器外部抛出的错误,如果在生成器内部没有进行错误捕捉(没有定义try/catch),生成器会被关闭。
  • 如果在生成器内部有进行错误捕捉,则不会影响影响后续遍历。但它跳过当前因throw而暂停的yield表达式。或者说
    throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带自动执行一次next方法,这样手动调用的
    下一次next()就会停在是当前暂停的yield表达式的下一个yield表达式。没法拿到因throw捕捉错误而跳过的那个yield表达式的返回值了

2.5 yield*表达式

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;
  }
}

结论:

  • 如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明我想要返回该遍历器对象的内部值。这被称为yield表达式。如果不加,则表示我单纯就是要返回这个遍历器对象本身
  • yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for…of循环。
  • 任何数据结构只要有 Iterator 接口,就可以被yield*遍历。

2.6 异步迭代生成器

异步流程的控制是生成器的另一大特点和适用场景。
而在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)
})


代码解析:

  • 迭代器it启动后,第一次调用next其返回值的value为第一个yield表达式后面值,即foo的返回值,即一个异步请求的promise对象。
  • 通过p.then来等待promise决议的完成,若成功则该请求返回了数据,则再次调用next并将返回的数据作为参数传进去作为第一个yield表达式的整体值,并赋值给data

核心点:

获得Promise和生成器最有效的最自然的方法就是yield出来一个promise,然后通过这个promise控制生成器的迭代器的执行

你可能感兴趣的:(Javascript,js,javascript,es6)