@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 转译器用来捕获生成器状态的技术非常相似。