Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号(*);二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

function* helloWorldGenerator() {yield 'hello';yield 'world';return 'ending';
}var hw = helloWorldGenerator();

上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(helloworld),即该函数有三个状态:helloworldreturn 语句(结束执行)。

然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

hw.next()
// { value: 'hello', done: false }hw.next()
// { value: 'world', done: false }hw.next()
// { value: 'ending', done: true }hw.next()
// { value: undefined, done: true }

总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在function关键字后面。

yield 表达式

由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

遍历器对象的next方法的运行逻辑如下:

  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

  2. 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

  3. 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

  4. 如果该函数没有return语句,则返回的对象的value属性值为undefined

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

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

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

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

Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

(function (){yield 1;
})()
// SyntaxError: Unexpected number

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

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

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

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

与 Iterator 接口的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

var myIterable = {};
myIterable[Symbol.iterator] = function* () {yield 1;yield 2;yield 3;
};[...myIterable] // [1, 2, 3]

Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

function* gen(){// some code
}var g = gen();g[Symbol.iterator]() === g
// true

上面代码中,gen是一个 Generator 函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。

next 方法的参数

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

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

注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。

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属性为truefor...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

利用for...of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。

function* objectEntries(obj) {let propKeys = Reflect.ownKeys(obj);for (let propKey of propKeys) {yield [propKey, obj[propKey]];}
}let jane = { first: 'Jane', last: 'Doe' };for (let [key, value] of objectEntries(jane)) {console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

加上遍历器接口的另一种写法是,将 Generator 函数加到对象的Symbol.iterator属性上面。

function* objectEntries() {let propKeys = Object.keys(this);for (let propKey of propKeys) {yield [propKey, this[propKey]];}
}let jane = { first: 'Jane', last: 'Doe' };jane[Symbol.iterator] = objectEntries;for (let [key, value] of jane) {console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。

function* numbers () {yield 1yield 2return 3yield 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

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语句捕获。

throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。

var g = function* () {try {yield;} catch (e) {console.log(e);}
};var i = g();
i.next();
i.throw(new Error('出错了!'));
// Error: 出错了!(…)

注意,不要混淆遍历器对象的throw方法和全局的throw命令。上面代码的错误,是用遍历器对象的throw方法抛出的,而不是用throw命令抛出的。后者只能被函数体外的catch语句捕获。

var g = function* () {while (true) {try {yield;} catch (e) {if (e != 'a') throw e;console.log('内部捕获', e);}}
};var i = g();
i.next();try {throw new Error('a');throw new Error('b');
} catch (e) {console.log('外部捕获', e);
}
// 外部捕获 [Error: a]

上面代码之所以只捕获了a,是因为函数体外的catch语句块,捕获了抛出的a错误以后,就不会再继续try代码块里面剩余的语句了。

如果 Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获。

var g = function* () {while (true) {yield;console.log('内部捕获', e);}
};var i = g();
i.next();try {i.throw('a');i.throw('b');
} catch (e) {console.log('外部捕获', e);
}
// 外部捕获 a

如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行。

var gen = function* gen(){yield console.log('hello');yield console.log('world');
}var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined

throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。

function* gen() {try {yield 1;} catch (e) {console.log('内部捕获');}
}var g = gen();
g.throw(1);
// Uncaught 1

上面代码中,g.throw(1)执行时,next方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。这种行为其实很好理解,因为第一次执行next方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时throw方法抛错只可能抛出在函数外部。

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 函数内部部署了try...catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历。

throw命令与g.throw方法是无关的,两者互不影响。

Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的catch捕获。

function* foo() {var x = yield 3;var y = x.toUpperCase();yield y;
}var it = foo();it.next(); // { value:3, done:false }try {it.next(42);
} catch (err) {console.log(err);
}

上面代码中,第二个next方法向函数体内传入一个参数 42,数值是没有toUpperCase方法的,所以会抛出一个 TypeError 错误,被函数体外的catch捕获。

一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefineddone属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

function* g() {yield 1;console.log('throwing an exception');throw new Error('generator broke!');yield 2;yield 3;
}function log(generator) {var v;console.log('starting generator');try {v = generator.next();console.log('第一次运行next方法', v);} catch (err) {console.log('捕捉错误', v);}try {v = generator.next();console.log('第二次运行next方法', v);} catch (err) {console.log('捕捉错误', v);}try {v = generator.next();console.log('第三次运行next方法', v);} catch (err) {console.log('捕捉错误', v);}console.log('caller done');
}log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done

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 }

如果return方法调用时,不提供参数,则返回值的value属性为undefined

function* gen() {yield 1;yield 2;yield 3;
}var g = gen();g.next()        // { value: 1, done: false }
g.return() // { 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 }

next()、throw()、return() 的共同点

next()是将yield表达式替换成一个值。

const g = function* (x, y) {let result = yield x + y;return result;
};const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;

throw()是将yield表达式替换成一个throw语句。

gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

return()是将yield表达式替换成一个return语句。

gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;

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 函数。

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"

从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*表达式。

let delegatedIterator = (function* () {yield 'Hello!';yield 'Bye!';
}());let delegatingIterator = (function* () {yield 'Greetings!';yield* delegatedIterator;yield 'Ok, bye.';
}());for(let value of delegatingIterator) {console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."

如果yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。

function* gen(){yield* ["a", "b", "c"];
}gen().next() // { value:"a", done:false }

实际上,任何数据结构只要有 Iterator 接口,就可以被yield*遍历。

let read = (function* () {yield 'hello';yield* 'hello';
})();read.next().value // "hello"
read.next().value // "h"

如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。

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}

作为对象属性的 Generator 函数

如果一个对象的属性是 Generator 函数,可以简写成下面的形式。

let obj = {* myGeneratorMethod() {···}
};

Generator 函数的this

Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。

function* g() {}g.prototype.hello = function () {return 'hi!';
};let obj = g();obj instanceof g // true
obj.hello() // 'hi!'

但是,如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象。

function* g() {this.a = 11;
}let obj = g();
obj.next();
obj.a // undefined

Generator 函数也不能跟new命令一起用,会报错。

function* F() {yield this.x = 2;yield this.y = 3;
}new F()
// TypeError: F is not a constructor

那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this

下面是一个变通方法。首先,生成一个空对象,使用call方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。

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,但是生成的对象实例是obj,有没有办法将这两个对象统一呢?

一个办法就是将obj换成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

再将F改成构造函数,就可以对它执行new命令了。

function* gen() {this.a = 1;yield this.b = 2;yield this.c = 3;
}function F() {return gen.call(gen.prototype);
}var f = new F();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

Generator 与上下文

JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。

这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。

Generator 函数不是这样,它执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

ES6——Generator 函数的语法相关推荐

  1. ES6学习(九)—Generator 函数的语法

    ES6学习(九)-Generator 函数的语法 Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同. Generator函数是一个状态机,内部封装了不同状态的 ...

  2. [ES6] Generator 函数

    [ES6] Generator 函数 Generator 函数与普通函数的区别 执行机制 Generator 函数返回的遍历器对象的方法 循环遍历器 Iterator 对象的方法 next 方法 re ...

  3. Generator函数的语法以及异步的应用

    基本概念 Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同.Generator函数有多种理解角度.语法上,首先可以把它理解成,Generator函数是一个状态机,封 ...

  4. “睡服”面试官系列第十八篇之generator函数的语法(建议收藏学习)

    目录 1简介 1.1基本概念 1.2yield 表达式 1.3与 Iterator 接口的关系 2. next 方法的参数 3. for...of 循环 4. Generator.prototype. ...

  5. es6 Generator函数的应用

    Generator函数的应用 es6 Generator 可以暂停函数执行,返回任意表达式的值.这种特点使得 Generator 有多种应用场景. 异步操作的同步化表达 Generator函数的暂停执 ...

  6. es6 Generator函数概述

    概述 什么是Generator函数 Generator函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同.本章详细介绍 Generator函数的语法和 API,它的异步编程应用请看 ...

  7. ES6 Generator函数

    一.基本概念 Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同. Generator 函数有多种理解角度. 语法上,Generator 函数是一个状态机,封装 ...

  8. 理解 ES6 Generator 函数

    Generator函数是ES6引入的新型函数,用于异步编程,跟Promise对象联合使用的话会极大降低异步编程的编写难度和阅读难度. Generator函数跟普通函数的写法有非常大的区别: 一是,fu ...

  9. es6 async函数的语法

    async函数的语法 async函数的语法规则总体上比较简单,难点是错误处理机制. 返回 Promise 对象 async函数返回一个 Promise 对象. async函数内部return语句返回的 ...

最新文章

  1. 返回页面顶部最简单方法
  2. Nginx下安装配置PageSpeed模块,轻松完成网站提速
  3. 7-8 德才论 (25 分)(C语言实现)
  4. 阿里云技术天团空降 CSDN 独家在线峰会,揭秘核心竞争力
  5. http工作原理和机制
  6. 软件测评师--第九小时 web应用测试
  7. jet-cp4005,linux双面打印,HP LaserJet Pro M706n 双面打印 技术规格 | HP®People's Republic of China...
  8. iOS客户端如何测试推送
  9. Kile 2.1.3 发布,TeX/LaTeX 集成编辑器
  10. 永远不要忽视 粉红色/红色的异样字体 在你不知道为什么跟你期望偏差那么大的时候,,不要急记得去问问为什么
  11. 【操作系统】成组链接法详解
  12. opencv4+contrib 编译
  13. pytorch训练FER2013模型
  14. 使用Unity编写传统ARPG游戏人物操作方式(二)
  15. 通过freemarker模板,使用jsoup将html转换为word,包含图片
  16. 为什么 Vue 更符合这个时代的大势所趋
  17. Linux 两台主机之间建立信任关系方式及基本原理
  18. 鸢尾花分类python算法_BP算法鸢尾花分类
  19. 计算机桌面截图怎么截,电脑如何截图,教您电脑截图怎么截
  20. 自家的摇头扇线掉了 (电风扇的五根线怎么接)

热门文章

  1. 5G计费方式将迎来彻底变化 运营商向2B服务出手?
  2. 河南城镇化争植“智慧”基因
  3. NTFS系统的访问控制上的权限条目
  4. js获取自定义属性的值
  5. 通达信里的统计函数及区块背景函数
  6. Android TextView 使用替换构建出不同样式的字符串
  7. GridView模板问题
  8. MVC3、如何应用EntityFramework 连接MySql 数据库
  9. ifconfig输出网口和ip
  10. Tomcat的安装及配置。