概念
-
Generator
函数是ES6
提供的一种异步编程解决方案,语法行为与传统函数完全不同。 -
Generator
函数是一个状态机,封装了多个内部状态。执行Generator
函数会返回一个遍历器对象 xxx,可以使用 xxx.next() 依次遍历Generator
函数内部的每一个状态。
-
特征
-
function
关键字与函数名之间有一个星号(不能用于箭头函数)。 -
函数体内部使用
yield
表达式,定义不同的内部状态。
function* generatorTest() { yield 'test'; return 'ending'; } // 调用这个方法,返回一个 迭代器 const gt = generatorTest();
-
-
调用
gt.next() // { value: 'detanx', done: false } gt.next() // { value: 'ending', done: true } gt.next() // { value: undefined, done: true }
-
第一次调用,
Generator
函数开始执行,直到遇到第一个yield
表达式为止。next
方法返回一个对象,它的value
属性就是当前yield
表达式的值hello
,done
属性的值false
,表示遍历还没有结束。 -
第二次调用,
Generator
函数从上次 yield 表达式停下的地方,一直执行到return
语句(如果没有return
语句,就执行到函数结束)。done
属性的值true
,表示遍历已经结束。 -
第三次调用,此时
Generator
函数已经运行完毕,next
方法返回对象的value
属性为undefined
,done
属性为true
。以后再调用next
方法,返回的都是这个值。
-
-
写法
-
function
关键字与函数名之间的*
未规定,所以有很多写法,我们写得时候最好还是使用第一种,即*
紧跟着function
关键字后面,*
后面再加一个空格。function* foo(x, y) { ··· }
-
yield 表达式
-
Generator
函数返回的遍历器对象,只有调用next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
-
next
方法的运行逻辑- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回对象的value
属性值。 - 下一次调用
next
方法时,再继续往下执行,直到遇到下一个yield
表达式。 - 如果没有再遇到新的
yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面表达式的值,作为返回对象的value
属性值。 - 如果该函数没有
return
语句,则返回的对象的value
属性值为undefined
。
- 遇到
-
定义 Generator 函数的时候,
yield
后面的表达式不会立即执行,只有当调用next
方法、内部指针指向该语句时才会执行。下面的123 + 456
不会立即求值,只有当执行next
到对应的yield
表达式才会求值。function* gen() { yield 123 + 456; }
-
yield
表达式只能用在Generator
函数里面,用在其他地方都会报错。(function (){ yield 1; // SyntaxError: Unexpected number })()
-
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 } // 参数和表达式右边 function* demo() { foo(yield 'a', yield 'b'); // OK let input = yield; // OK }
与 Iterator 接口的关系
-
与
Iterator
接口的关系-
任意一个对象的
Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。 -
Generator
函数就是遍历器生成函数,可以把Generator
赋值给对象的Symbol.iterator
属性,从而使得该对象具有Iterator
接口。var myIterable = {}; myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; return 4; }; [...myIterable] // [1, 2, 3],遍历只会遍历 yield 后面的值
-
-
Generator
函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator
属性,执行后返回自身。function* gen(){ // some code } var g = gen(); g[Symbol.iterator]() === g // true
-
for...of
循环-
for...of
循环可以自动遍历Generator
函数运行时生成的Iterator
对象,且此时不再需要调用next
方法。一旦next
方法的返回对象的done
属性为true
,for...of
循环就会中止,且不包含该返回对象。function* numbers() { yield 1; yield 2; return 3; } for (let v of numbers()) { console.log(v); } // 1 2
-
-
其它
-
扩展运算符(
...
)、解构赋值和Array.from
方法内部调用的,都是遍历器接口。它们都可以将Generator
函数返回的Iterator
对象,作为参数。function* numbers() { yield 1; yield 2; return 3; } // 扩展运算符 [...numbers()] // [1, 2] // Array.from 方法 Array.from(numbers()) // [1, 2] // 解构赋值 let [x, y] = numbers(); x // 1 y // 2
-
next 方法的参数
-
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作 上一个yield
表达式的返回值。const g = function* (x, y) { let result = yield x + y; return result; }; const gen = g(1, 2); gen.next(); // {value: 3, done: false} gen.next(1); // {value: 1, done: true} // 相当于将 let result = yield x + y // 替换成 let result = 1;
-
由于
next
方法的参数表示上一个yield
表达式的返回值,所以在第一次使用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 } 42 = 5 + 24 + 13
throw()、return()
共同点
- 它们的作用都是让
Generator
函数恢复执行,并且使用不同的语句替换yield
表达式。
-
throw()
是将yield
表达式替换成一个throw
语句。const g = function* (x, y) { let result = yield x + y; return result; }; const gen = g(1, 2); gen.throw(new Error('出错了')); // Uncaught Error: 出错了 // 相当于将 let result = yield x + y // 替换成 let result = throw(new Error('出错了'));
-
return()
是将yield
表达式替换成一个return
语句。gen.return(2); // {value: 2, done: true} // 相当于将 let result = yield x + y // 替换成 let result = return 2;
不同点
-
Generator.prototype.throw()
-
Generator
函数返回的遍历器对象,throw
方法可以在函数体外抛出错误,然后在Generator
函数体内捕获。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
语句捕获。 -
如果
Generator
函数内部没有部署try...catch
代码块,那么throw
方法抛出的错误,将被外部try...catch
代码块捕获。var g = function* () { while (true) { yield; // 找不到这里的 e,报错 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(); // hello g.throw(); // Uncaught undefined
-
throw
方法抛出的错误要被内部捕获,前提是必须至少执行过一次next
方法。g.throw(1)
执行时,next
方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。function* gen() { try { yield 1; } catch (e) { console.log('内部捕获'); } } var g = gen(); g.throw(1); // Uncaught 1
-
Generator
函数体外抛出的错误,可以在函数体内捕获;反过来,Generator
函数体内抛出的错误,也可以被函数体外的catch
捕获。function* foo() { var x = yield 3; // 数值是没有 toUpperCase 方法的,所以会抛出一个 TypeError 错误,被函数体外的catch捕获。 var y = x.toUpperCase(); yield y; } var it = foo(); it.next(); // { value:3, done:false } try { it.next(42); } catch (err) { console.log(err); }
-
-
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() // { 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 } 因为 return 参数为 7,所以 value 为 7
-
yield* 表达式
-
yield*
表达式用来在一个Generator
函数里面执行另一个Generator
函数。function* foo() { yield 'a'; yield 'b'; } function* bar() { yield 'x'; yield* foo(); yield 'y'; } for (let v of bar()){ console.log(v); } // "x" // "a" // "b" // "y"
-
如果
yield*
后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。yield
命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。function* gen(){ yield* ["a", "b", "c"]; yield 1 return 'end' } let g = gen() g.next() // { value:"a", done:false } g.next() // { value:"b", done:false } g.next() // { value:"c", done:false } g.next() // {value: 1, done: false} g.next() // {value: "end", done: true}
-
任何数据结构只要有
Iterator
接口,就可以被yield*
遍历。let read = (function* () { yield 'hello'; yield* 'hello'; })(); read.next().value // "hello" read.next().value // "h" read.next().value // "e" read.next().value // "l" read.next().value // "l" read.next().value // "o" read.next().value // undefined
-
如果被代理的
Generator
函数有return
语句,那么就可以向代理它的Generator
函数返回数据。下面例子中函数foo
的return
语句,向函数bar
提供了返回值。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 函数写法
let obj = {
* myGeneratorMethod() {
···
}
};
// 等价于
let obj = {
// *位置可以在 function 关键字和括号之间任意位置
myGeneratorMethod: function* () {
// ···
}
};
Generator 函数的 this
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
-
Generator
函数g
返回的遍历器obj
,是g
的实例,而且继承了g.prototype
。但是,把g
当作普通的构造函数,并不会生效,因为g
返回的总是遍历器对象,而不是this
对象。 -
Generator 函数也不能跟 new 命令一起用,会报错。
function* F() { yield this.x = 2; yield this.y = 3; } new F() // TypeError: F is not a constructor
-
变通方法。首先,生成一个空对象,使用
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
。再将 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
-
应用
-
取出嵌套数组的所有成员
function* iterTree(tree) { if (Array.isArray(tree)) { for(let i=0; i < tree.length; i++) { yield* iterTree(tree[i]); } } else { yield tree; } } const tree = [ 'a', ['b', 'c'], ['d', 'e'] ]; for(let x of iterTree(tree)) { console.log(x); } // a // b // c // d // e
-
遍历完全二叉树
// 下面是二叉树的构造函数, // 三个参数分别是左树、当前节点和右树 function Tree(left, label, right) { this.left = left; this.label = label; this.right = right; } // 下面是中序(inorder)遍历函数。 // 由于返回的是一个遍历器,所以要用generator函数。 // 函数体内采用递归算法,所以左树和右树要用yield*遍历 function* inorder(t) { if (t) { yield* inorder(t.left); yield t.label; yield* inorder(t.right); } } // 下面生成二叉树 function make(array) { // 判断是否为叶节点 if (array.length == 1) return new Tree(null, array[0], null); return new Tree(make(array[0]), array[1], make(array[2])); } let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]); // 遍历二叉树 var result = []; for (let node of inorder(tree)) { result.push(node); } result // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
-
Ajax 的异步操作
- 通过
Generator
函数部署Ajax
操作,可以用同步的方式表达。注意,makeAjaxCall
函数中的next
方法,必须加上response
参数,因为yield
表达式,本身是没有值的,总是等于undefined
。
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();
- 通过
-
逐行读取文本文件。
function* numbers() { let file = new FileReader("numbers.txt"); try { while(!file.eof) { yield parseInt(file.readLine(), 10); } } finally { file.close(); } }
-
控制流管理
- 如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); }); });
- 利用
for...of
循环会自动依次执行yield
命令的特性,提供一种更一般的控制流管理的方法。
let steps = [step1Func, step2Func, step3Func]; function* iterateSteps(steps){ for (var i=0; i< steps.length; i++){ var step = steps[i]; yield step(); } }
-
部署 Iterator 接口
- 利用
Generator
函数,可以在任意对象上部署Iterator
接口。
function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; } } let myObj = { foo: 3, bar: 7 }; for (let [key, value] of iterEntries(myObj)) { console.log(key, value); } // foo 3 // bar 7
- 利用
-
作为数据结构
- Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构。
function* doStuff() { yield fs.readFile.bind(null, 'hello.txt'); yield fs.readFile.bind(null, 'world.txt'); yield fs.readFile.bind(null, 'and-such.txt'); } for (task of doStuff()) { // task是一个函数,可以像回调函数那样使用它 }
网友评论