ECMAScript 6(21)Generator 函数的语法
程序员文章站
2022-06-13 08:00:30
...
1. 是什么
- 调用 Generator 函数后,该函数并不执行
- 调用后不反回结果, 返回一个对象, 遍历器对象(Iterator Object)。
- 对象调用next()方法,进入下一步. 简单说, Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
- 如果没有return语句,就执行到函数结束
- done属性是一个布尔值,表示是否遍历结束。done属性的值true,表示遍历已经结束。
- yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
function* test(){
yield 'hello';
yield 'world';
return 'end';
}
var tt = test()
tt.next() // {value: "hello", done: false}
tt.next() // {value: "world", done: false}
tt.next() // {value: "end", done: false}
tt.next() // {value: undefined, done: true}
- Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
console.log('执行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
- 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
}
- yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
- 任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
- Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function* gen(){
yield 1;
yield 2;
}
var g = gen();
[...g] // [1,2]
2. next()
- next方法的参数表示上一个yield表达式的返回值
- 所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
3. for…of
- for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
- 一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for…of循环之中。
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
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循环
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
4. Generator.prototype.throw()
- Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
- throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。
- throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。
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
- throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
- 一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。简单说就是里面报错没有在里面catch到,函数就直接结束.
- 遍历器的throw方法可以将错误传入Generator函数内部;
- Generator函数内部可以通过try…catch来捕获错误,如果没有被捕获,将冒泡到Generator函数外面来;
- 如果在Generator函数内部正常捕获了错误,那么代码将继续执行下去;
- 如果没有捕获到错误,遍历器的done将变为true;
5.Generator.prototype.return()
- Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。
- return方法调用时,不提供参数,则返回值的value属性为undefined
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 }
- Generator 函数内部有try…finally代码块,且正在执行try代码块,那么return方法会导致立刻进入finally代码块,执行完以后,整个函数才会结束。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
6. yield*
如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
// 手动遍历 foo()
for (let i of foo()) {
console.log(i);
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// x
// a
// b
// y
- ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
- yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for…of循环。
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"
- 有return语句时,则需要用var value = yield* iterator的形式获取return语句的值。
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
7. 作为对象属性的 Generator 函数
let obj = {
* myGeneratorMethod() {
···
}
// 等同于
myGeneratorMethod: function* () {
// ···
}
};
8. this
- Generator函数的this指向的是window
function *g() {
console.log(this)
yield 'g'
}
let foo = g()
foo.next()
// Window
// {value: "g", done: false}
- Generator函数不能通过new来生成遍历器(会报错),所以改变this指向之可以用 call,apply,bind
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
- 另一种是直接绑在 F.prototype 上
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
9. 状态机
- 这里的状态机通常指有限状态机。
- javascript 通常的操作就是定义一个全局变量flag , 然后通过flag = !flag 改变状态,有点类似与jquery 里面的toggle操作
var flag = true;
var toggle = function(){
if(flag)
// show
else
// hide
flag = !flag
}
- 使用 Generator 实现
var toggle = function* () {
while (true) {
// show
yield;
// hide
yield;
}
};
- 优点不用定于全局状态变量, 这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。
10. 应用
- Generator函数的主要目的就是用于解决异步编程,可以极大的减轻异步编程的撰写难度。
- 举例 假如我们有5个异步请求,并且后一个需要根据前一个异步请求的结果来完成请求,那么代码的复杂度将急剧上升。
function delay(msg, callback) {
setTimeout(function () {
console.log(msg)
callback()
}, 1000)
}
delay('1', function () {
delay('2', function () {
delay('3', function () {
delay('4', function () {
delay('5', function () {
console.log('done')
})
})
})
})
})
- es6新增的promise,在解决异步编程时,降低了一定难度,但主要是降低了对于单个异步请求的难度,看起来简单轻松。也增强了对错误处理的能力,但对于以上情况的解决,并没有质的提升。
function delay(msg) {
return new Promise((resolve, reject) => {
setTimeout(function () {
console.log(msg)
resolve()
}, 1000)
})
}
delay('1')
.then(() => delay('2'))
.then(() => delay('3'))
.then(() => delay('4'))
.then(() => delay('5'))
.then(() => {
console.log('done')
})
– 上面代码耦合度高,而且要求每个异步都是Promise,不然不可行;
– 于是,Generator函数出现了,是解决此类场景的极佳办法;
function delay(msg) {
setTimeout(function () {
iter.next(msg)
}, 1000)
}
function*g() {
let result1 = yield delay('1')
console.log(result1)
let result2 = yield delay('2')
console.log(result2)
let result3 = yield delay('3')
console.log(result3)
let result4 = yield delay('4')
console.log(result4)
let result5 = yield delay('5')
console.log(result5)
console.log('done')
}
let iter = g()
iter.next()
- 这个方法具备以下特点:
- 像同步代码一样写多个异步请求,同步编程写法显然比Promise或者回调函数套用要简单;
- 解耦,每个异步函数在最后加上一个iter.next()方法,用于将状态机推到下一个状态,但却不必关心下一个状态是哪个异步请求;
- 通过iter.next()的参数,可以将上一次请求的数据传给下一次请求的数据,简单暴力;
- 通过函数的参数(比如dalay(result1))来将上一次异步请求的结果传给当前这次异步请求使用。
10.2 部署遍历器接口
let obj = {
a: 1,
b: 2,
c: 3,
*[Symbol.iterator]() {
yield this.c
yield this.b
yield this.a
}
}
let arr = [...obj];
arr; // [3, 2, 1]
参考文献:阮一峰es6
上一篇: vue 嵌套路由 页面空白问题
推荐阅读
-
ES6中的迭代器、Generator函数及Generator函数的异步操作方法
-
es6 Generator函数的应用实例讲解
-
es6 generator函数的异步编程
-
es6 Class的Generator函数实例讲解
-
ES6 - 基础学习(21): Generator 函数
-
ECMAScript6 新的数据结构、Class语法、Proxy和Promise,看完我写的ES6,马上会的,你就是高手
-
ECMAScript 6(21)Generator 函数的语法
-
ECMAScript 6 Module的语法和加载实现
-
ECMAScript6入门第十九章-Generator函数的异步应用
-
ECMAScript6入门第十八章-Generator函数的语法