ES6 —(Generator 函数的语法)

1、简介

  Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。从语法上,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,即它还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
  Generator 函数有两个特征:一是,function 关键字与函数名之间有一个星号(*);二是,函数体内部使用 yield 表达式,定义不同的内部状态。
  Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号,不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,即遍历器对象。

function* helloWorld(){
    yield 'hello';
    yield 'world';
    return 'ending';
}
var ite = helloWorld();
(1)yield

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

yield 表达式与 return 表达式对比:
  1)相同之处,都能返回紧跟在语句后面的那个表达式的值。
  2)区别在于,每次遇到 yield ,函数暂停执行,下一次在从该位置继续向后执行。而 return 语句不具备位置记忆功能。
  3)一个函数里面,只能执行一次(或者说一个) return 语句,但是可以执行多次(或者说多个)yield 表达式。因此,Generator 函数可以返回一系列值。

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

function* f(){
    console.log("执行了!");
}  
var generator = f();
generator.next();  // 执行了!

  2)yield 表达式只能在 Generator 函数里,用在其他地方会报错。

function f(){
    yield "err!";  // Unexpected identifier
}

  3)yield 表达式如果用在另一个表达式中,必须放在圆括号内。

function* foo(){
    console.log('hello' + yield);  // Unexpected identifier
    console.log('hello' + yield 123);  // Unexpected identifier
    console.log('hello' + (yield));   // 正确
    console.log('hello' + (yield 123)); // 正确
}

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

function* foo(){
    fn(yield 'a', yield 'b'); // 正确
    var y = yield; // 正确
} 

  5)yield 表达式本身没有返回值,或者说总是返回 undefined。

function* foo(){
    var a = yield 1;
    console.log("a = " + a);
}
var fn = foo();
console.log(fn.next()); 
console.log(fn.next()); 
输出结果:
Object {value: 1, done: false}
a = undefined 
Object {value: undefined, done: true}
(2)与 Iterator 接口的关系

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

var my ={};
my[Symbol.iterator] =function*  (){
    yield 1;        
    yield '2';
    yield 3;
    yield 4 ;
};
var a = [...my];
console.log(a);  // [1, "2", 3, 4]

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

function* foo(){}
var fn = foo();
console.log(fn[Symbol.iterator]() === fn); // true

2、next 方法

遍历器对象的 next 方法的运行逻辑如下:
  (1)调用 next 方法,内部指针就从函数头部或者上一次停下来的地方开始执行,遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的表达式的值,作为返回对象的 value 属性值。({value: , done: false})。
  (2)下一次调用 next 方法时,继续往下执行,直到遇到下一个 yield 表达式。
  (3)如果没有遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回对象的 value 属性值({value: , done: true})。
  (4)如果该函数没有 return 语句,则返回的对象 value 属性值为 undefined ({value: undefined , done: true})。

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

function* foo(){
    var a = yield 1;
    console.log("a = " + a);
}
var fn = foo();
console.log(fn.next()); 
console.log(fn.next(2)); // 将 “yield 1” 表达式的返回值置为 2
输出结果:
Object {value: 1, done: false}
a = 2 // yield 1 返回值被置为 2 所以 a 被赋值为 2
Object {value: undefined, done: true}

  Generator 函数从暂停状态到恢复运行,它的上下文状态时不变的。通过 next 方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内注入值,也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数的行为。

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

3、for…of 循环

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

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

注意:
  一旦 next 方法返回对象的 done 属性为 true,for...of 循环就会终止,且不会包含该返回对象,所以上面代码的 return 语句返回的 5 不包括在 for...of 循环之中。

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

function* objectEntries(obj){
    let propkeys = Reflect.ownKeys(obj);
    for(let key of propkeys){
        yield [key, obj[key]];
    }
}

var obj = {first: 'Jone', last: 'Doe'};
for(let [key, value] of objectEntries(obj)){
    console.log(`(${key}, ${value})`);
} // (first, Jone)  (last, Doe)

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

4、Generator.prototype.throw()

  Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数内部捕获。
  Generator.prototype.throw() 方法可以接受一个参数,该参数会被 Generator 函数内部 catch 语句接收,建议抛出 Error 对象实例。

function* foo(){
    try{
        yield console.log("try");
    } catch(e) {
        console.log(`内部错误${e}`);
    }
    yield console.log("foo");
}
var fn = foo();
fn.next();  // try
fn.throw(new Error("错误!")); // 内部错误Error: 错误!  foo

注意:
  (1)throw 方法被捕获后,会附带执行下一条 yield 表达式,也就是说会附带执行一次 next 方法。所以上面代码在 catch 捕获到错误后继续执行到下一个 yield 处。
  (2)不要混淆遍历器对象的 throw 方法和全局的 throw 方法。
  全局 throw 抛出的错误后,被 Generator 函数外部 catch 捕获后,就不会再继续执行 try 代码块里剩余的语句了,并且全局抛出的错误不会影响遍历器的状态。
  而遍历器对象的 throw 方法抛出错误后,被 Generator 函数内部 catch 捕获后 还会继续执行 try 代码内部语句。
  (3)如果 Generator 函数内部内部没有部署 try...catch 代码块,那么 throw 方法抛出的错误,将被外部 try...catch 代码块捕获。

var fn = foo();
fn.next(); // try
try{
    fn.throw(new Error("错误1")); 
    fn.throw(new Error("错误2"));
} catch(e) {
    console.log(`外部错误${e}`);
}
输出结果:
try
内部错误Error: 错误1
foo
外部错误Error: 错误2

从上面代码可以看出,“错误1” 处抛出错误后,被 Generator 内部 catch 捕获,但是并没有跳出 try...catch 代码块,而是继续执行,抛出了 ”错误2“。由于抛出 ”错误1“ 时,执行了 next 方法,所以 Generator 内部执行到了 yield console.log("foo"); 处,接着抛出 ”错误2“ 时,已经没有相应的 catch 语句可以捕获错误,于是被外部 catch 语句捕获。
  (4)如果 Generator 函数内部和外部,都没有部署 try...catch 代码块,那么程序将报错,直接中断执行。
  (5)Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的 catch 捕获。

function* bar(){
    yield a;
    yield console.log('try 2');
} 
var barf = bar();
try{
    barf.next();
} catch(e){
    console.log(`外部 ${e}`);
} // 外部 ReferenceError: a is not defined

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

5、Generator.prototype.return()

  Generator.prototype.return() 方法可以返回给定的值,并且终结遍历 Generator 函数。

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

注意:
  (1)如果 return 方法调用时,不提供参数,则返回值的 value 属性为 undefined 。

var gn = gen();
console.log(gn.next()); // Object {value: 1, done: false} 
console.log(gn.return()); // Object {value: undefined, done: true}   

  (2)如果 Generator 函数内部有 try...finally 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。

function* numbers () {
    yield 1;
    try {
        yield 2;
        yield 3;
    } finally {
        yield 4;
        yield 5;
    }
    yield 6;
}
var g = numbers();

console.log(g.next()); 
console.log(g.next());
console.log(g.return(7));
console.log(g.next());
console.log(g.next());
输出结果:
Object {value: 1, done: false}
Object {value: 2, done: false}
Object {value: 4, done: false}
Object {value: 5, done: false}
Object {value: 7, done: true}

6、yield* 表达式

  如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。如果在 Generator 函数内部执行另一个 Generator 函数,需要用到 yield* 表达式。

function* foo(){
    yield 1;
    yield 2;
}
--------------------------------
function* bar1(){
    yield 3;
    foo();
    yield 4;
}
var g1 = bar1();    
console.log(g1.next()); // Object {value: 3, done: false}
console.log(g1.next()); // Object {value: 4, done: false}
console.log(g1.next()); // Object {value: undefined, done: true} 
---------------------------------------
function* bar2(){
    yield 3;
    yield* foo();
    yield 4;
}
var g2 = bar2();    
console.log(g2.next()); // Object {value: 3, done: false}
console.log(g2.next()); // Object {value: 1, done: false}
console.log(g2.next()); // Object {value: 2, done: false}
console.log(g2.next()); // Object {value: 4, done: false}
console.log(g2.next()); // Object {value: undefined, done: true}     

  yield* 等同于在 Generator 函数内部,部署一个 for...of 循环,来遍历 yield* 后面的 Generator 函数(实际上,任何数据结构只要有 Iterator 接口,都可以被 yield* 遍历)。

yield* foo() 
=> for(let value of foo()){
        yield value;
}

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

var v = yield* foo(); 
用于接收 foo 函数内 return 回的数据。

使用 yield* 语句遍历完全二叉树。

// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
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']

7、作为对象属性的 Generator 函数

  如果一个对象的属性是 Generator 函数,可以简写

let obj = {
    * myGeneratorMethod(){}
}
等价于
let obj = {
    myGeneratorMethod(): function* (){}
}

8、Generator 函数的 this

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

function* foo(){}
foo.prototype.hello = function (){
    console.log("hello");   
}
var fn = foo();
console.log(fn instanceof foo); // true
fn.hello(); // hello

注意:
  (1)Generator 函数返回的是一个遍历器对象,不是 this 对象,这与普通构造函数不同。因此它拿不到 this 对象上的属性。

function* foo(){
    this.a = 11;
}
let fn = foo();
console.log(fn.a); // undefined

  (2)Generator 函数不能跟 new 命令一起用,会报错。因为 Generator 函数不是构造函数。
  (3)如果要取到 Generator 函数 this 对象的属性,应该在调用 Generator 函数时,使用 call 或 apply 方法绑定一个对象,然后通过该对象来获取 this 对象上的属性。

function* foo(){
    this.a = 1;
    yield this.b = 2;
}
function F(){
    return foo.call(foo.prototype);
}
var f = new F();
console.log(f.next());;  // Object {value: 2, done: false}
console.log(f.next());;  // Object {value: undefined, done: true}

console.log(f.a); // 1
console.log(f.b); // 2

阮一峰:ECMAScript 6入门

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值