Javascript学习笔记-生成器

2017-11-06  本文已影响0人  Patrick浩
Javascript-生成器.png

在Javascript中,普通函数一旦开始运行在函数运行结束之前是不会中断的,而ES6引入的Generator(生成器)可以使得函数可以发生中断,分步运行。

1 Iterable和Iterator

在说明如何创建和使用Generator之前,先要说到ES6中迭代相关问题。

1.1 Iterable

在ES6中,可以使用for of的方式来循环遍历对象:

for(let i of [1,2,3,4]){
    console.log(i);
}

一个对象如果能够进行这样的迭代循环就被称为iterable(可迭代)。在iterable的对象中包含一个属性[Symbol.iterator],这个属性的值是一个function,而这个function是一个iterator(迭代器)

var obj = {
    [Symbol.iterator]:function iterator(){
        // TODO 省略掉了iterator的实现
    }
}

1.2 Iterator

一个iterator是返回指一个包含next属性,其值为是一个方法,且调用该next方法会返回对象符合{value:xx, done: xx}结构的值。

var iterator = function() {
    let i = 0;
    return {
        next() {
            i++;
            return {value:i, done: i > 3}
        }
    }
}

1.3 ES6中的迭代

自定义iterable对象的完整创建过程如下:

var o = {
    [Symbol.iterator]: function() {
        let i = 0;
        return {
            next() {
                i++;
                return {value:i, done: i > 3}
            }
        }
    }
}

在使用for of进行迭代操作的时候,每一次循环都会调用iteratornext函数,将每一次next调用返回的value作为迭代的值,同时会一直迭代直到done的值返回true为止

for(let i of o) {
    console.log(i); // 1, 2, 3 因为当i>3的时候done返回为true,所以就停止执行
}

同时ES6中引入了[...x](分离操作)的语法糖,可以将iterable对象迭代的结果值放到一个数组中:

var arr = [...o]; 
console.log(arr); // [1,2,3]

2. Generator的创建

生成器自身是iterable的,同时自身是一个iterator对象

2.1 使用function *()创建

生成器通常使用function *()的方式进行创建,声明中*的位置可以直接跟在function关键字后面也可以在函数名前面,创建的生成器函数中包含yield关键字。

function *g() { // 也可以是function* g()
  yield 10;
}

2.2 使用GeneratorFunction构造方法创建

也可以使用GeneratorFunction构造方法的形式创建生成器,不过用起来就比较复杂了,其中GeneratorFunction(...arguments, exec)接受的最后一个参数exec为生成器方法体,前面的参数为函数构造器的参数

var GeneratorFunction = Object.getPrototypeOf(function *(){}).constructor;
var g = new GeneratorFunction('a', 'yield a');
// 上面的构造过程相当于
function *g(a) {
  yield a;
}

通常使用function *()声明,因为看起来更适合阅读。

3 Generator的使用

3.1 获取iterator

在使用生成器的时候,直接调用构造方法,就可以返回一个iterator(迭代器)

function *g() {
  yield 1;
}
var it = g(); // 生成一个iterator

之前说过Generator其自身是iterable的同时是iterator,主要表现在:

function *g(){
  yield 1;
}
var it = g();
console.log(it[Symbol.iterator]() === it);

3.2 顺序化异步执行

构造了迭代器对象iterator后,和普通的函数不同,生成器函数并没有进行执行,只有在调用了next方法(也可以是return或者throw)方法以后,才会开始执行,且按照以下方式可以判断执行结果:

  1. 执行到第一个yiled停止,返回{value: x, done: x}格式的数据,其中将yield关键字后的值作为value的值输出,并将done设置为false,再次调用next方法,函数会再次向后执行
  2. 如果调用next方法以后函数向后执行不存在yield,则此时valueundefined(或者return的值),done将设置为true
  3. 如果调用next方法以后函数向后执行存在return关键字,则此时valuereturn的值,done将设置为true

// 生成器最后不返回值
function *g1() {
  var a = yield 3;
  console.log(a);
}
var it1 = g1();
console.log(it1.next()); // {value: 3, done: false};
console.log(it1.next()); // {value: undefined, done: true};
// 生成器有return语句
function *g2() {
  var a = yield 3;
  console.log(a);
  return 2;
}
var it2 = g2();
console.log(it2.next()); // {value: 3, done: false};
console.log(it2.next()); // {value: 2, done: true};

除了通过next方法可以获取到yield之后到值以外,next方法可以添加一个参数,参数将把yield表达式替换为该参数值

function *g1() {
  var a = yield 3;
  console.log('a=' + a);
}
var it1 = g1();
it1.next(); // 运行到`yield 3`停止,并返回{value: 3, done: false};
it1.next(1); // 继续运行,同时把`yield 3`替换为1,输出生成器函数中console.log('a=' + a)语句

要注意三点:

如果调用return,迭代器会立即结束,value值为return的值,done设置为true

function *g(){
  yield 1;
  yield 2;
} 
var it = g();
console.log(it.return(3)); // {value: 3, done: true};
console.log(it.next()); // {value: undefined, done: true} 

如果生成器方法中存在return语句,执行到return语句后会将返回值作为value的值,且done会变为true,继续调用next方法,不能获取到yield的值,value始终为undefineddone始终为true

function *g(){
  yield 1;
  return 2;
  yield 3;
  return 4;
}
var it = g();
console.log(it.next()); // {value: 1, done: false};
console.log(it.next()); // {value: 2, done: true};
console.log(it.next()); // {value: undefined, done: true};
console.log(it.next()); // {value: undefined, done: true};

如果调用迭代器的throw方法或者生成器函数中存在throw操作,将抛出异常,迭代器运行结束

function *g() {
  yield 1;
  throw 2;
}
var it = g();
console.log(it.next()); // {value: 1, done: false};
console.log(it.next()); // 提示异常,并终止运行
// 或者
function *g() {
  yield 1;
  yield 2;
}
var it = g();
console.log(it.throw(2)); // 提示异常,并终止运行
console.log(it.next()); // 不会执行
3.2.1 在异步方法中的使用

生成器因为可以进行中断运行,且利用next方法控制运行,所以对于异步方法,就可以使用顺序的方式来编写代码,而不用在回调中进行数据的处理。

// 普通方法执行
function f() {
  var a;
  setTimeout(_=> {
    a = 3;
  }, 1000);
  var b = a * 100; 
  console.log('b=' + b);
}
f(); // 输出b=NaN
// 使用生成器执行
function *g() {
  var a = yield setTimeout(_=> {
    it.next(3);
  }, 1000);
  var b = a * 100; // 
  console.log('b=' + b);
}
var it = g();
it.next(); // 1s后会输出b=300
3.2.2 和Promise的联合用法

和在异步方法中的控制过程一样,可以在Promise中使用来控制程序的执行

function *g() {
  var p = yield Promise.resolve(3).then(fulfill => {
    setTimeout(_=>{
      it.next(fulfill);
    }, 1000)
  });
  console.log('p=' + p);
}
var it = g();
it.next(); // 1s后输出 p=3

3.3 [Symbol.iterator]的迭代器

由于生成器的特性,于是用来创建iterable对象的iterator

var obj = {
  [Symbol.iterator]: function *g() {
    let i = 0;
    while(i < 3) {
      i++;
      yield i;
    }
    return 'finish' + i;
  }
}
for(let o of obj) {
  console.log(o); // 1, 2, 3
}

4 其他

4.1 形实转换程序

在Javascript中使用一个函数来来封装其他函数,并将需要的参数也一并封装

function f(x, y){
  return x + y;
}
function thunk() {
  return f(3, 4);
}
thunk(); // 返回7

对于存在回调的函数,可以采用增加一个回调函数作为参数

function f(x, y, cb){
  setTimeout(_=>{
    cb(x, y);
  },1000)
}
function thunk(cb) {
  return f(3, 4, cb);
}
thunk((x, y)=>{
  console.log(x+y); // 7;
});

利用这种写法,我们可以对很多异步方法进行重新封装,可以让我们的焦点放到回调函数中,可以一定程度将参数输入和输出的处理过程进行分开开发。

4.2 生成器委托

在生成器中,可以嵌套iterable对象,嵌套iterable对象的时候并使用yield*,当执行到yield* iterable的时候,会将运行流程交给iterable对象的iterator。并在继续执行时调用其next方法。

function *g1() {
  yield 2;
  yield 3;
}
function *g2() {
  yield 1;
  yield* g1(); // 或者yield *g1();
  yield 4;
}
var it = g2();
console.log(it.next()); // {value: 1, done: false};
console.log(it.next()); // {value: 2, done: false};
console.log(it.next()); // {value: 3, done: false};
console.log(it.next()); // {value: 4, done: false};
console.log(it.next()); // {value: undefined, done: true};

5 总结

生成器是一个很有意思的东西,使用生成器可以将异步操作进一步升级,用一种同步编码风格去处理异步的情况。

6 参考

《你不知道的Javascript(中卷)》
MDN Generator

上一篇下一篇

猜你喜欢

热点阅读