ES6:Generator函数详解

1、 概念

Generator函数式ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。对于Generator函数有多种理解角度。
从语法上:可以把它理解成一个状态机,封装了多个内部状态;
从形式上:Generator 函数是一个普通函数,但是有两个特征:一是function命令与函数名之间有一个星号*;二是函数体内部使用yield语句定义不同的内部状态。

// 传统函数
function fn() {
	return 'hello world'
}
fn()  // 'hello world',一旦调用立即执行

// Generator函数
function* helloWorldGenerator () {
	yield 'hello'
	yield 'world'
	return 'ending'
}

const hw = helloWorldGenerator()  // 调用 Generator函数,函数并没有执行,返回的是一个Iterator对象
console.log(hw.next())  // {value: 'hello', done: false},value 表示返回值,done 表示遍历还没有结束
console.log(hw.next())  // {value: 'world', done: false},value 表示返回值,done 表示遍历还没有结束
console.log(hw.next())  // {value: 'ending', done: true},value 表示返回值,done 表示遍历还没有结束
console.log(hw.next())  // {value: undefined, done: true},value 表示返回值,done 表示遍历结束

上面的代码中可以看到传统函数和Generator函数的运行是完全不同的,传统函数调用后立即执行并输出了返回值;Generator函数则没有执行而是返回一个Iterator对象,并通过调用Iterator对象的next方法来遍历,每次调用Iterator对象的next方法时,内部的指针就会从函数的头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式或return语句暂停。换句话说,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而 next方法可以恢复执行。
执行过程如下:

  • 第1次调用,Generator 函数开始执行,直到遇到第一条 yield 语句为止。next 方法返回一个对象,它的 value 属性就是当前 yield 语句的值 hellodone 属性的值 false 表示遍历还没有结束。
  • 第2次调用,Generator 函数从上次 yield 语句停下的地方,一直执行到下一条 yield 语句。next 方法返回的对象的 value 属性就是当前 yield 语句的值 worlddone 属性的值false 表示遍历还没有结束。
  • 第 3 次调用,Generator 函数从上次 yield 语句停下的地方,一直执行到 return 语句(如果没有 return 语句,就执行到函数结束)。next 方法返回的对象的 value 属性就是紧跟在return 语句后面的表达式的值(如果没有 return 语句,则 value 属性的值为 undefined),done 属性的值 true 表示遍历已经结束。
  • 第 4 次调用,此时 Generator 函数已经运行完毕,next 方法返回的对象的 value 属性为undefineddone 属性为 true。以后再调用 next 方法,返回的都是这个值。

2、yield表达式

由于 Generator 函数返回的遍历器对象只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield 语句就是暂停标志。
遍历器对象的 next 方法的运行逻辑如下。
1.遇到 yield 语句就暂停执行后面的操作,并将紧跟在 yield 后的表达式的值作为返回的对象的 value 属性值。
2.下一次调用 next 方法时再继续往下执行,直到遇到下一条 yield 语句。
3.如果没有再遇到新的 yield 语句,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值作为返回对象的value 属性值。
4.如果该函数没有 return 语句,则返回对象的 value 属性值为undefined

注意:只有调用next方法且内部指针指向该语句时才会执行yield语句后面的表达式,因此,等于为JavaScript提供了手动的“惰性求值”的语法功能。

function* gen() {
	yield 123 + 456;
}

上面的代码中,yield 后面的表达式 123 + 456 不会立即求值,只会在 next 方法将指针移到这一句时才求值。

2.1 yield 语句与 return 语句区别

yield 语句与 return 语句既有相似之处,又有区别。相似之处在于都能返回紧跟在语句后的表达式的值。区别在于每次遇到 vield 函数暂停执行,下一次会从该位置继续向后执行而 return 语句不具备位置记忆的功能。一个函数里面只能执行一次(或者说一条) return语句,但是可以执行多次(或者说多条) yield 语句。正常函数只能返回一个值,因为只能执行一次 return 语句;Generator 函数可以返回一系列的值,因为可以有任意多条 yield 语句。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是其名称的来历(在英语中generator”这个词是“生成器”的意思)。

2.2 Generator函数不加yield语句,这时变成了一个单纯的暂缓执行函数

function* f () {
	console.log('执行了')
}
var generator = f()
setTimeout(function () {
	generator.next()
}, 2000)

上面的代码中,函数 f 如果是普通函数,在为变量 generator 赋值时就会执行。但是函数 f是一个 Generator 函数,于是就变成只有调用 next 方法时才会执行。

2.3 yield 表达式只能用在 Generator 函数里面,用在其它地方都会报错

{
  (function (){
    yield 1;
  })()

  // SyntaxError: Unexpected number
  // 在一个普通函数中使用yield表达式,结果产生一个句法错误
}

2.4 yield 表达式如果用在另一个表达式中,必须放在圆括号里面

{
	function* demo() {
		console.log('Hello' + yield); // SyntaxError
		console.log('Hello' + yield 123); // SyntaxError
		
		console.log('Hello' + (yield)); // OK
		console.log('Hello' + (yield 123)); // OK
	}
}

2.5 yield 表达式用作参数或放在赋值表达式的右边,可以不加括号

{
	function* demo() {
		foo(yield 'a', yield 'b'); // OK
		let input = yield; // OK
	}
}

3、与 Iterator 接口的关系

任意一个对象的 Symbol.iterator 方法等于该对象的遍历器对象生成函数,调用该函数会返回该对象的一个遍历器对象。
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。

var myIterable = {}
myIterable[Symbol.iterator] = function* () {
	yield 1
	yield 2
	yield 3
}
 for(let value of myIterable) {
 	console.log(value)
 }
 // 1
 // 2
 // 3
[...myIterable]  // [1, 2, 3]

上面的代码中,Generator 函数赋值给 Symbol.iterator 属性,从而使得 myIterable对象具有了 Iterator 接口,可以被...运算符遍历。

4、next() 方法的参数

yield 语句本身没有返回值,或者说总是返回 undefinednext 方法可以带有一个参数,该参数会被当作上一条 yield 语句的返回值。

function* f () {
    for (var i = 0; true; i++) {
		var reset = yield i
    	if (reset) {
      		console.log('执行了')
      		i = -1
   		 }
  	 }
}
var g = f()
g.next()  // {value: 0, done: false}
g.next()  // {value: 1, done: false}
g.next(true)  // {value: 0, done: false}

上面的代码先定义了一个可以无限运行的 Generator 函数 f,如果 next 方法没有参数,每次运行到 yield 语句时,变量 reset 的值总是 undefined。当next 方法带有一个参数 true时,当前的变量 reset 就被重置为这个参数(即 true),因而 i 会等于-1,下一轮循环就从-1开始递增。

这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,其上下文状态(context)是不变的。通过 next 方法的参数就有办法在Generator 函数开始运行后继续向函数本内部注入值。也就是说,可以在 Generator 函数运行的不同阶段从外部向内部注入不同的值,从而调整函数行为。
再看一个例子。

function* foo (x) {
	var y = 2 * (yield (x + 1))
	var z = yield (y / 3)
	return (x + y + z)
}

var a = foo(5)
a.next()  // 首次调用next,函数只会执行到 “yield(5+1)” 暂停,并返回 {value: 6, done: false}
a.next()  // 第二次调用next,没有传递参数,所以 y的值是undefined,那么 y/3 当然是一个NaN,所以应该返回 {value: NaN, done: false}
a.next()  // 同样的道理,z也是undefined,6 + undefined + undefined = NaN,返回 {value: NaN, done: true}

var b = foo(5)
b.next()  // 正常的运算应该是先执行圆括号内的计算,再去乘以2,由于圆括号内被 yield 返回 5 + 1 的结果并暂停,所以返回{value: 6, done: false}
b.next(12)  // 上次是在圆括号内部暂停的,所以第二次调用 next方法应该从圆括号里面开始,就变成了 let y = 2 * (12),y被赋值为24,所以第二次返回的应该是 24/3的结果 {value: 8, done: false}
b.next(13)  // 参数2被赋值给了 z,最终 x + y + z = 5 + 24+ 13 = 42,返回 {value: 42, done: true}

5、for…of循环

for...of 循环可以自动遍历 Generator 函数生成的 Iterator 对象,且此时不再需要调用next 方法。

function* foo () {
	yield 1
	yield 2
	yield 3
	yield 4
	yield 5
	return 6
}
for (let v of foo()) {
	console.log(v)
}
// 1 2 3 4 5

上面的代码使用 for...of 循环依次显示 5条 yield 语句的值。
注意:一旦 next 方法的返回对象的 done 属性为 true,for…of 循环就会终止,且不包含该返回对象,所以上面的 return 语句返回的 6 不包括在 for…of 循环中。

6、Generator.prototype.throw()

Generator 函数返回的遍历器对象都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

var g = function* () {
	try (
		yield;
	} catch (e) {
		console.log('内部捕获’,e);
	}
}var i=g();
i.next();

try {
	i.throw('a');
	i.throw('b');
} catch (e) {
	console.log('外部捕获’,e);
}
//内部捕获a
// 外部捕获b

上面的代码中,遍历器对象 i 连续抛出两个错误。第一个错误被 Generator 函数体内的catch 语句捕获。i 第二次抛出错误,由于 Generator 函数内部的 catch 语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的 catch语句捕获。

7、Generator.prototype.return()

Generator 函数返回的遍历器对象还有一个 return 方法,可以返回给定的值,并终结Generator 函数的遍历。

function* gen() {
	yield 1;
	yield 2;
	yield 3;
}
var g = gen();
g.next ()  // { value: 1, done: false }
g.return('foo')  // { value:"foo",done: true }
g.next()  // { value: undefined, done: true }

上面的代码中,遍历器对象 g 调用 return 方法后,返回值的 value 属性就是 return方法的参数 foo。同时,Generator 函数的遍历终止,返回值的 done 属性为 true,以后再调用 next 方法,done 属性总是返回 true
如果 return 方法调用时不提供参数,则返回值的 vaule 属性为 undefined

function* gen() {
	yield 1;
	yield 2;
	yield 3;
}
var g = gen();
g.next()  // { value: 1,done: false }
g.return()  // { value: undefined,done: true }

8、yield*表达式

如果在 Generator函数内部调用另一个 Generator 函数,默认情况下是没有效果的。

function* foo() {
	yield 'a';
	yield'b';
}
function* bar() {
	yield'x';
	foo();
	yield 'y';
}
for (let v of bar()){
	console.log(v);
}
//"x"
//"y"

上面的代码中,foobar 都是 Generator 函数,在 bar 里面调用 foo 是不会有效果的。

这时就需要用到 yield*语句,用来在一个 Generator 函数里面执行另一个 Generator 函数。

function* bar() {
	yield 'x';
	yield* foo();
	yield'y';
}
// 等同于
function* bar() [
	yield 'x';
	yield 'a';
	yield 'b';
	yield 'y';
}
// 等同于
function* bar() {
	yield 'x';
	for (let v of foo()) {
		yield v;
	}
	yield 'y';
}
for (let v of bar()) {
	console.log(v);
}
// 'x';
// 'a';
// 'b';
// 'y';

9、Generator函数应用举例

9.1 利用 Generator 函数和 for…of 循环实现斐波那契数列

function* fibonacci() {
	let [prev,curr] = [01];
	for (;;) {
		[prev, curr] = [curr, prev + currl;
		yield curr;
	}
}
for (let n of fibonacci()) {
	if (n > 1000) break;
	console.log(n);
}
// 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

9.2 异步操作的同步化表达

Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。

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();

上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。
注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined

9.3 逐行读取文本文件

function* numbers() {
	let file = new FileReader("numbers.txt");
	try {
		while(!file.eof) {
			yield parseInt(file.readLine()10);
		}
	} finally {
		file.close();
	}
}

上面的代码打开文本文件,使用 yield 语句可以手动逐行读取文件。

9.4 控制流管理

如一个多步操作非常耗时,采用回调的话:

step1(function (value1) {
	step2(value1, function(value2) {
		step3(value2, function(value3) {
			step4(value3, function(value4) {
				// Do something with value4
			});
		});
	});
});

采用Promise改写上面的代码如下:

Promise.resolve(step1)
	.then(step2)
	.then(step3)
	.then(step4)
	.then(function (value4) {
	// Do something with value4
	}, function (error) {
	// Handle any error from step1 through step4
	}).done();

上面的代码已经把回调函数改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。

function* longRunningTask(valuel) {
	try {
		var value2 = yield stepl(valuel);
		var value3 = yield step2(value2);
		var value4 = yield step3(value3);
		var value5 = yield step4(value4);
		// Do something with value4
	} catch (e) {
		// Handle any error from stepl through step4
	}
}

9.5 Genarator 对任意对象部署Iterator接口

function* deployObjectInterface(obj){
    let keys = Object.keys(obj);
    for(let i=0; i<keys.length; i++){
        let key = keys[i];
        yield [key, obj[key]];
    }
}
let obj = {name:"jow", age:21 };
for(let[key, value] of deployObjectInterface(obj)){
    console.log(key, value); 
}
// name jow
// age 21

9.6 Genarator 对数组部署Iterator接口

function* deployArrayInterface(arr){
    var nextIndex = 0;
    while(nextIndex < arr.length){
        yield arr[nextIndex++];
    }
}
var arr = deployArrayInterface(['name', 'age']);

console.log(arr.next());       // {value: "name", done: false}
console.log(arr.next().value); // name
console.log(arr.next().done);  // false

console.log(arr.next().value); // age
console.log(arr.next().done);  // true

console.log(arr.next().value); // undefined
console.log(arr.next().done);  // true

10、promise、generator和aysnc/await区别

三者都是异步编程的解决方案,不同的是,promise为较早出来的,其次generator,最后为async/await,三者象征了前端进行解决异步编程的进化路程。

10.1 promise

 promise比较简单,也是最常用的,主要就是将原来用回调函数异步编程的方法转成relsove和reject触发事件;
    对象内含有四个方法,then()异步请求成功后
	    catch()异步请求错误的回调方法
	    finally()请求之后无论是什么状态都会执行
	    resolve()将现有对象转换为Promise对象
	    all()此方法用于将多个Promise实例包装成一个新的promise实例。
	    race()也是将多个Promise实例包装成一个新的promise实例
	    reject()返回一个状态为Rejected的新Promise实例。
    优点:让回调函数变成了规范的链式写法,程序流程可以看的很清楚
    缺点:编写的难度比传统写法高,阅读代码也不是一眼可以看懂

10.2 Generator

generator是一个迭代生成器,其返回值为迭代器(lterator),ES6标准引入的新的数据类型,主要用于异步编程,它借鉴于Python中的generator概念和语法;

generator函数内有两个重要方法,1 yield表达式 2.next()

Generator 函数是分段执行的,yield表达式是暂停执行的标记,而 next方法可以恢复执行

优点:1.利用循环,每调用一次,就使用一次,不占内存空间 2.打破了普通函数执行的完整性
缺点: 需要用next()方法手动调用,直接调用返回无效iterator 2.

10.3 async/await

   
async:异步函数
await:同步操作

es7中提出来的异步解决方法,是目前解决异步编程终极解决方案,以promise为基础,其实也就是generator的高级语法糖,本身自己就相当于一个迭代生成器(状态机),它并不需要手动通过next()来调用自己,与普通函数一样

async就相当于generator函数中的*,await相当于yield,

async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。
function getSomething() {
	return "something";
}

async function testAsync() {
	return Promise.resolve("hello async");
}

async function test() {
	//await是在等待一个async函数完成
	const v1 = await getSomething();
	//await后面不仅可以接Promise,还可以接普通函数或者直接量
	const v2 = await testAsync();
	console.log(v1, v2);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端~初学者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值