js generator函数特性

  1. js generator函数特性
    1. 基本使用
    2. 递归输出
    3. next传参
  2. 和iterator接口的关系
  3. yield* 表达式
  4. generator函数异步应用
    1. thunkify
    2. co

js generator函数特性

  • 函数声明function函数名之间加*号,*只要在中间,中间有空格也无所谓
  • yield 关键字后面跟上返回值,使用.next()调用,输出{value: VALUE, done: [falise|true]}
  • done状态为true之后,value值就全都是undefined了,valueundefined不能被for of循环获取到
  • 同一个作用域中函数不能被重复调用,执行完一遍之后 内部的done变为 true之后就不能再调用了
  • 如果一个对象实现了iterator属性,且这个属性是一个generator函数,则generator函数的yield的值可以作为遍历的值

基本使用

generator函数的调用方式和普通函数不一样,generator函数直接调用会返回一个迭代器对象,该迭代器对象可以使用for进行循环,也可以使用next()进行调用,区别是使用for循环调用的输出是yield关键字后面的返回值,而使用next()的输出是{value: VALUE, done: [falise|true]}对象

注意,使用for循环调用的时候,不会遍历return后面的值;

function* generator () {
  yield 1
  yield 2
  return 'ending'   // return 标记结束,但还可以调用,return的值不会在for of循环中被获取到
}

let g = generator()
console.log(g.next());  // { value: 1, done: false }
console.log(g.next());  // { value: 2, done: false }
console.log(g.next());  // { value: 'ending', done: true }

let g2 =generator()
for (let a of g2) {
  console.log(a);
}
// 1
// 2

console.log(g.next());  // { value: undefined, done: true }
// 在同一个作用域中,多次调用generator函数,一旦done的状态为true了之后,函数就不能再调用了,而使用next()强行调用,会返回`{ value: undefined, done: true }`

递归输出

我们可以写一个递归函数,自己自动调用next,进行输出

function* generator () {
  yield 1
  yield 2
  return 'ending'
}
let g = generator()

// 递归执行,自己写的递归会打印ending,for of 不会
function next () {
  let { value, done } = g.next();
  console.log(value);
  if (!done) {
    next()
  }
}
next();

next传参

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

先看第一个例子,不传参的情况

function* say (num) {
  let x = 2 * (yield (num + 1));
  console.log(x);

  let y = x * (yield (num + 2));
  console.log(y);

  return 'ending'
}

const g1 = say(1);0
console.log(g1.next())  // { value: 2, done: false }
console.log(g1.next())  // NaN
                        // { value: 3, done: false }
console.log(g1.next())  // NaN
                        // { value: 'ending', done: true }
  • 第一个nextnum1,碰到yield就退出,输出{ value: 2, done: false }
  • 第二个nextnum1,由于没有传入参数,则yield返回值是undefinedx等于 2 * undefinedx结果是NaN,执行console.log(x),输出NaN,然后碰到yield退出,输出{ value: 3, done: false }
  • 第三个nextnum1,由于没有传入参数,y等于 NaN * NaNy结果是NaN,然后return{ value: 'ending', done: true }

再来看看传参的情况

const g2 = say(1);
console.log(g2.next(1))   // { value: 2, done: false }
console.log(g2.next(5))   // 10
                          // { value: 3, done: false }
console.log(g2.next(10))   // 100
                          // { value: 'ending', done: true }
  • 第一个nextnum1,传入了参数1,但是没用,在此次yield之前没有yield,所以没用,然后碰到yield退出,输出{ value: 2, done: false }
  • 第二个nextnum1,传入了参数5,,x结果是2 * 5(这里的第二个5next传入的5),x的结果是10,执行console.log(x),输出10,然后碰到yield退出,输出{ value: 3, done: false }
  • 第三个nextnum1,传入了参数10y等于10 * 10y结果是100,然后return{ value: 'ending', done: true }

和iterator接口的关系

实际上可以通过for循环遍历的对象都有一个Symbol.iterator属性,该属性就是一个generator函数,可以直接进行遍历,就有Symbol.iterator

如果我们把数组对象的Symbol.iterator属性修改了,则数组就无法进行遍历了

我们还可以改写Symbol.iterator属性,使其按照我们想要的形式输出,由于Symbol.iterator属性就是一个generator函数,我们则可以给它赋值一个自定义generator函数

我们都知道,普通的key-value对象是不具备迭代能力的,因此不能直接进行for循环,需要使用Object.keys或者Object.values函数辅助进行遍历,我们可以把generator赋值给对象的Symbol.iterator属性,从而使得该对象具有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

yield* 表达式

ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。

function* foo() {
  yield 'a';
  yield 'b';
}

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"

以上示例来自阮一峰的ES6教程

使用generator实现二叉树遍历

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

console.log(result)
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']

以上示例来自阮一峰的ES6教程

generator函数异步应用

javascript的异步操作都是使用回调函数的方式进行的,但是会造成回调地狱的问题,后来generator函数出现了,配合thunk函数和执行器可以实现同步操作,而不使用回调函数,再结合后面的async/await,可以使我们用同步的形式写异步代码

thunkify

Thunk 函数是自动执行Generator函数的一种方法。Thunk函数不是函数类型,而是一种编程方式,把多个入参的函数转换为一个入参的方式

例如

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

thunk函数在generator函数中的使用,一般使用thunkify模块

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);

var gen = function* () {
    var r1 = yield readFileThunk('./data/a.text');
    var r2 = yield readFileThunk('./data/b.text');
    console.log(r1.toString());
    console.log(r2.toString());
};

// 手动的执行
var g = gen();
var r1 = g.next();
console.log(r1);        // { value: [Function], done: false }   vale值就是yield出来的thunk函数
r1.value(function (err, data) {   // 执行thunk函数的回调函数, data就是readFileThunk的结果,然后把结果传入给generator
    if (err) throw err;
    // console.log(data.toString()); // aaaaa, 但是一般不在执行器里面处理结果,执行器仅进行流程控制,一般是把得到的结果传给generator,在generator里面进行处理
    var r2 = g.next(data); // 把结果data传入generator函数中
    r2.value(function (err, data) {
        if (err) throw err;
        g.next(data);
    });
});

generator自动执行器,自动管理需要自己写一个方法,递归的进行自动处理,或者使用下文介绍的co模块

function run(fn) {
    var gen = fn();
    function next(err, data) {
        var result = gen.next(data);
        if (result.done) return;
        result.value(next);
    }
    next();
}

run(gen)

上面这种方式是同步输出的

co

co模块是著名程序员TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于Generator 函数的自动执行。效果和自己写的run方法作用一样,但是co内部进行了很多处理,逻辑更加严谨,在co4.0版本中,yield不仅可以是一个thunk函数,也可以是一个Promise对象

var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
var co = require('co')

var gen = function* () {
    var r1 = yield readFileThunk('./data/a.text');
    var r2 = yield readFileThunk('./data/b.text');
    console.log(r1.toString());
    console.log(r2.toString());
};

co(gen)

co还支持并发的异步操作,就是把各个一步操作放在一个数组里面,当所有的操作执行成功后,再进行下一步

var fs = require('fs');
var co = require('co')

var readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function (error, data) {
            if (error) reject(error);
            resolve(data.toString());
        });
    });
};

// 数组的写法
co(function* () {
    var res = yield [
        readFile('./data/a.text'),
        readFile('./data/b.text')
    ];
    console.log('res', res);
}).catch();

// 对象的写法
co(function* () {
    var res = yield {
        1: Promise.resolve(1),
        2: Promise.resolve(2),
    };
    console.log(res);
}).catch();

输出结果

{ '1': 1, '2': 2 }
res [ 'aaaaa', 'bbbbb' ]

由于都是异步操作,虽然读取文件是在Promise.resolve前面,但是读取文件是需要时间的,所以对象写法的输出会在前面


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com