写在开头
ES6
常用但被忽略的方法 系列文章,整理作者认为一些日常开发可能会用到的一些方法、使用技巧和一些应用场景,细节深入请查看相关内容连接,欢迎补充交流。
相关文章
- ES6常用但被忽略的方法(第一弹解构赋值和数值)
- ES6常用但被忽略的方法(第二弹函数、数组和对象)
- ES6常用但被忽略的方法(第三弹Symbol、Set 和 Map )
- ES6常用但被忽略的方法(第四弹Proxy和Reflect)
- ES6常用但被忽略的方法(第五弹Promise和Iterator)
- ES6常用但被忽略的方法(第七弹async)
- ES6常用但被忽略的方法(第八弹Class)
- ES6常用但被忽略的方法(第九弹Module)
- ES6常用但被忽略的方法(第十弹项目开发规范)
- ES6常用但被忽略的方法(第十一弹Decorator)
- ES6常用但被忽略的方法(终弹-最新提案)
Generator
基本概念
Generator
函数是ES6
提供的一种异步编程解决方案,语法行为与传统函数完全不同。语法上,Generator
函数是一个状态机,封装了多个内部状态。执行Generator
函数会返回一个遍历器对象,可以依次遍历Generator
函数内部的每一个状态。
- 特征
function
关键字与函数名之间有一个星号;- 函数体内部使用
yield
表达式,定义不同的内部状态。
function* detanxGenerator() { yield 'detanx'; return 'ending'; } const dg = detanxGenerator();
- 调用
dg.next() // { value: 'detanx', done: false } dg.next() // { value: 'ending', done: true } dg.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) { ··· } function*foo(x, y) { ··· } function *foo(x, y) { ··· } function * foo(x, y) { ··· }
yield 表达式
Generator
函数返回的遍历器对象,只有调用next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
next
方法的运行逻辑
(1)遇到yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。
(2)下一次调用next
方法时,再继续往下执行,直到遇到下一个yield
表达式。
(3)如果没有再遇到新的yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。
(4)如果该函数没有return
语句,则返回的对象的value
属性值为undefined
。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接口和for...of 循环的关系
- 与
Iterator
接口的关系
- 上一弹ES6常用但被忽略的方法(第五弹Promise和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
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
- 除了
for...of
循环
- 扩展运算符(
...
)、解构赋值和Array.from
方法内部调用的,都是遍历器接口。它们都可以将Generator
函数返回的Iterator
对象,作为参数。
// 扩展运算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers();
x // 1
y // 2
next 方法的参数
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
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 }
-
上面代码中,第二次运行
next
方法的时候不带参数,导致y
的值等于2 * undefined
(即NaN
),除以3
以后还是NaN
,因此返回对象的value
属性也等于NaN
。第三次运行next
方法的时候不带参数,所以z
等于undefined
,返回对象的value
属性等于5 + NaN + undefined
,即NaN
。 -
如果向
next
方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next
方法时,返回x+1
的值6
;第二次调用next
方法,将上一次yield
表达式的值设为12
,因此y
等于24
,返回y / 3
的值8
;第三次调用next
方法,将上一次yield
表达式的值设为13
,因此z
等于13
,这时x
等于5
,y
等于24
,所以return
语句的值等于42
。 -
由于
next
方法的参数表示上一个yield
表达式的返回值,所以在第一次使用next
方法时,传递参数是无效的。
next()、throw()、return()
共同点
- 它们的作用都是让
Generator
函数恢复执行,并且使用不同的语句替换yield
表达式。
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;
不同点
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; 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
方法。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; var y = x.toUpperCase(); yield y; } var it = foo(); it.next(); // { value:3, done:false } try { it.next(42); } catch (err) { console.log(err); }
- 数值是没有
toUpperCase
方法的,所以会抛出一个TypeError
错误,被函数体外的catch
捕获。
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 }
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"];
}
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
函数返回数据。下面例子中函数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 = {
myGeneratorMethod: function* () { // *位置可以在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是一个函数,可以像回调函数那样使用它 }
- 异步应用