3

在阅读 javascript 博客和文章时,我发现对 ES6 生成器很感兴趣,但我无法理解它们在本质上与由一组函数组成的当前序列有何不同。例如,下面的工厂将采取一系列功能步骤并在步骤之间产生。

function fakeGen(funcList) {
    var i = 0, context;
    return function next() {
        if (i<funcList.lenght) {
            return {value: funcList[i++](context)}
        } else return {done:true}
    }
}

我缺少什么好处以及转译器如何在 ES6 中实现魔法?

4

2 回答 2

2

生成器本质上是一个枚举器函数,它允许在您调用它时更改您正在操作的上下文,实际上它与您的函数数组之间没有太大区别,但是您获得的优势是它不必是正在评估的函数中的函数,从而简化了闭包。举个例子:

function* myGenerator() {
    for (var i = 0; i < arr.length; i++) {
        yield arr[i];
    }
}

这是一个非常简单的示例,但不必构建您需要提供给某人以枚举结果的上下文,它是为您提供的,并且您可以确保该done属性在完成之前将是错误的。这个函数看起来比你给出的例子干净得多。最大的优势可能是围绕此的优化可以在幕后进行,因此对象内存占用得到了优化。

一个不错的感觉是,在枚举多个对象集合时,您真正清理了代码,如下所示:

function* myGenerator() {
    for (var i = 0; i < arr.length; i++) {
        yield arr[i];
    }
    for (var i = 0; i < arr2.length; i++) {
        yield arr2[i];
    }
    yield* myGenerator2();
}

只使用链式嵌套函数可以完成同样的事情,但是代码的可维护性和可读性会受到一些影响。

就转译器而言,来自 CS 线程:

没有冲突。Coffeescript 只会生成它需要的任何 javascript 来编译它使用的任何语法,无论是旧的还是新的。
过去,在所有浏览器都支持之前,coffeescript 不会使用任何 javascript 功能。这可能也适用于生成器。在此之前,您将需要使用反引号。

我对大多数转译器的一般理解是,他们在实现不会回溯且通常兼容的功能时必须小心,因此通常会迟到。

就像你说的,生成器并没有做任何特别的事情,它只是使编码更易于阅读、维护、使用或性能更好的语法糖。

于 2015-02-08T01:21:07.087 回答
2

@tophallen是对的。您可以完全在 ES3/ES5 中实现相同的功能。但语法不一样。让我们举一个例子,希望能解释为什么语法很重要。

ES6 生成器的主要应用之一是异步操作。有几个 跑步者被设计用来包装产生一系列Promises的生成器。当一个包装的生成器产生一个 Promise 时,这些运行器会等到该 Promise 被解决或拒绝,然后恢复生成器,将结果传回或使用iterator.throw().

一些跑步者,比如tj/co,还允许产生承诺数组,传回值数组。

这是一个例子。此函数并行执行两个 url 请求,然后将它们的结果解析为 JSON,以某种方式组合它们,将组合数据发送到其他 url,并返回(promise of an)答案:

var createSmth = co.wrap(function*(id) {
  var results = yield [
    request.get('http://some.url/' + id),
    request.get('http://other.url/' + id)
  ];
  var jsons = results.map(JSON.parse),
      entity = { x: jsons[0].meta, y: jsons[1].data };
  var answer = yield request.post('http://third.url/' + id, JSON.stringify(entity));
  return { entity: entity, answer: JSON.parse(answer) };
});

createSmth('123').then(consumeResult).catch(handleError);

请注意,此代码几乎不包含样板。大多数行执行上述描述中存在的一些操作。

还要注意缺少错误处理代码。所有的错误,无论是同步的(比如 JSON 解析错误)还是异步的(比如失败的 url 请求)都会被自动处理并且会拒绝产生的 promise。

如果您需要从某些错误中恢复(即防止它们拒绝生成的 Promise),或者使它们更具体,那么您可以用 a 包围生成器内的任何代码块,try..catch同步和异步错误都将在catch堵塞。

同样可以使用一组函数和一些辅助库(如async )来实现:

var createSmth = function(id, cb) {
  var entity;
  async.series([
    function(cb) {
      async.parallel([
        function(cb){ request.get('http://some.url/' + id, cb) },
        function(cb){ request.get('http://other.url/' + id, cb) }
      ], cb);
    },
    function(results, cb) {
      var jsons = results.map(JSON.parse);
      entity = { x: jsons[0].meta, y: jsons[1].data };
      request.post('http://third.url/' + id, JSON.stringify(entity), cb);
    },
    function(answer, cb) {
      cb(null, { entity: entity, answer: JSON.parse(answer) });
    }
  ], cb);
};

createSmth('123', function(err, answer) {
  if (err)
    return handleError(err);
  consumeResult(answer);
});

但这真的很丑。更好的主意是使用 Promise:

var createSmth = function(id) {
  var entity;
  return Promise.all([
    request.get('http://some.url/' + id),
    request.get('http://other.url/' + id)
  ])
  .then(function(results) {
    var jsons = results.map(JSON.parse);
    entity = { x: jsons[0].meta, y: jsons[1].data };
    return request.post('http://third.url/' + id, JSON.stringify(entity));
  })
  .then(function(answer) {
    return { entity: entity, answer: JSON.parse(answer) };
  });
};

createSmth('123').then(consumeResult).catch(handleError);

比使用生成器的版本更短、更简洁,但代码仍然更多。还有一些样板代码。注意这些.then(function(...) {行和var entity声明:它们不执行任何有意义的操作。

更少的样板(=generators)使您的代码更易于理解和修改,并且编写起来更有趣。这些是任何代码最重要的特征之一。这就是为什么很多人,尤其是那些习惯了其他语言中类似概念的人,对生成器如此欣喜若狂:)

switch关于你的第二个问题:转译器使用闭包、语句和状态对象来发挥他们的神奇魔力。例如,这个函数:

function* f() {
  var a = yield 'x';
  var b = yield 'y';
}

将由regenerator转换成这个(Traceur的输出看起来非常相似):

var f = regeneratorRuntime.mark(function f() {
  var a, b;
  return regeneratorRuntime.wrap(function f$(context$1$0) {
    while (1) switch (context$1$0.prev = context$1$0.next) {
      case 0:
        context$1$0.next = 2;
        return "x";
      case 2:
        a = context$1$0.sent;
        context$1$0.next = 5;
        return "y";
      case 5:
        b = context$1$0.sent;
      case 6:
      case "end":
        return context$1$0.stop();
    }
  }, f, this);
});

正如你所看到的,这里没有什么神奇的,生成的 ES5 相当简单。真正的魔力在于生成 ES5 的代码,即转译器的代码,因为它们需要支持所有可能的边缘情况。并且最好以产生高性能输出代码的方式执行此操作。

UPD:这是一篇可以追溯到 2000 年的有趣文章,描述了用纯 C 语言实现的伪协程 :) Regenerator 和其他 ES6 > ES5 转译器用来捕获生成器状态的技术非常相似。

于 2015-02-08T04:25:46.867 回答