ES6 生成器和函数数组之间的区别
Difference between ES6 Generators and Array of Functions
在阅读 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 中实现魔法?
生成器本质上是一个枚举器函数,它允许在调用它时更改您正在操作的上下文,实际上它与您的函数数组之间并没有太大区别,但是优点你得到的是它不必是正在评估的函数内部的函数,从而简化了闭包。举个例子:
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 线程:
There is no conflict. Coffeescript will just generate whatever javascript it needs to compile whatever syntax it uses, old or new.
In the past coffeescript would not use any javascript feature until all browsers support it. This will probably apply to generators also. Until then you will need to use backticks.
我对大多数转译器的一般理解是,它们在实现不会回溯且通常兼容的功能时必须小心,因此通常会迟到。
正如您所说,生成器并没有做任何特别的事情,它只是使代码更易于阅读、维护、使用或性能更好的语法糖。
是对的。您可以在 ES3/ES5 中完全实现相同的功能。但语法不一样。让我们举一个例子,它有望解释为什么语法很重要。
ES6 生成器的主要应用之一是异步操作。有several runners designed to wrap generators which produce a sequence of Promises。当包装的生成器产生一个承诺时,这些运行器会等到该承诺被解决或拒绝,然后恢复生成器,将结果传回或使用 iterator.throw()
.
在屈服点抛出异常
一些跑步者,如 tj/co,还允许产生承诺数组,传回值数组。
这里是例子。此函数并行执行两个 url 请求,然后将它们的结果解析为 JSON,以某种方式组合它们,将组合数据发送到其他 url,并且 returns (一个承诺) 答案:
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),或者使它们更具体,那么你可以用 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);
});
但这真的很难看。更好的主意是使用承诺:
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
声明:它们不执行任何有意义的操作。
更少的样板文件(=生成器)使您的代码更易于理解和修改,并且编写起来更有趣。这些是任何代码最重要的特征之一。这就是为什么很多人,尤其是那些习惯了其他语言中类似概念的人,对生成器如此狂热的原因:)
关于你的第二个问题:转译器使用闭包、switch
语句和状态对象来发挥他们的转化魔法。例如这个函数:
function* f() {
var a = yield 'x';
var b = yield 'y';
}
会被改造regenerator into this one (the output of Traceur看起来很像):
var f = regeneratorRuntime.mark(function f() {
var a, b;
return regeneratorRuntime.wrap(function f$(context[=14=]) {
while (1) switch (context[=14=].prev = context[=14=].next) {
case 0:
context[=14=].next = 2;
return "x";
case 2:
a = context[=14=].sent;
context[=14=].next = 5;
return "y";
case 5:
b = context[=14=].sent;
case 6:
case "end":
return context[=14=].stop();
}
}, f, this);
});
如您所见,这里没有什么神奇的,生成的 ES5 相当简单。真正的魔力在于生成 ES5 的代码,即转译器的代码,因为它们需要支持所有可能的边缘情况。并且最好以产生高性能输出代码的方式执行此操作。
UPD:here is an interesting article 可追溯到 2000 年,描述了纯 C 中伪协程的实现 :) Regenerator 和其他 ES6 > ES5 转译器使用的技术捕获生成器的状态非常相似。
在阅读 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 中实现魔法?
生成器本质上是一个枚举器函数,它允许在调用它时更改您正在操作的上下文,实际上它与您的函数数组之间并没有太大区别,但是优点你得到的是它不必是正在评估的函数内部的函数,从而简化了闭包。举个例子:
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 线程:
There is no conflict. Coffeescript will just generate whatever javascript it needs to compile whatever syntax it uses, old or new.
In the past coffeescript would not use any javascript feature until all browsers support it. This will probably apply to generators also. Until then you will need to use backticks.
我对大多数转译器的一般理解是,它们在实现不会回溯且通常兼容的功能时必须小心,因此通常会迟到。
正如您所说,生成器并没有做任何特别的事情,它只是使代码更易于阅读、维护、使用或性能更好的语法糖。
ES6 生成器的主要应用之一是异步操作。有several runners designed to wrap generators which produce a sequence of Promises。当包装的生成器产生一个承诺时,这些运行器会等到该承诺被解决或拒绝,然后恢复生成器,将结果传回或使用 iterator.throw()
.
一些跑步者,如 tj/co,还允许产生承诺数组,传回值数组。
这里是例子。此函数并行执行两个 url 请求,然后将它们的结果解析为 JSON,以某种方式组合它们,将组合数据发送到其他 url,并且 returns (一个承诺) 答案:
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),或者使它们更具体,那么你可以用 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);
});
但这真的很难看。更好的主意是使用承诺:
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
声明:它们不执行任何有意义的操作。
更少的样板文件(=生成器)使您的代码更易于理解和修改,并且编写起来更有趣。这些是任何代码最重要的特征之一。这就是为什么很多人,尤其是那些习惯了其他语言中类似概念的人,对生成器如此狂热的原因:)
关于你的第二个问题:转译器使用闭包、switch
语句和状态对象来发挥他们的转化魔法。例如这个函数:
function* f() {
var a = yield 'x';
var b = yield 'y';
}
会被改造regenerator into this one (the output of Traceur看起来很像):
var f = regeneratorRuntime.mark(function f() {
var a, b;
return regeneratorRuntime.wrap(function f$(context[=14=]) {
while (1) switch (context[=14=].prev = context[=14=].next) {
case 0:
context[=14=].next = 2;
return "x";
case 2:
a = context[=14=].sent;
context[=14=].next = 5;
return "y";
case 5:
b = context[=14=].sent;
case 6:
case "end":
return context[=14=].stop();
}
}, f, this);
});
如您所见,这里没有什么神奇的,生成的 ES5 相当简单。真正的魔力在于生成 ES5 的代码,即转译器的代码,因为它们需要支持所有可能的边缘情况。并且最好以产生高性能输出代码的方式执行此操作。
UPD:here is an interesting article 可追溯到 2000 年,描述了纯 C 中伪协程的实现 :) Regenerator 和其他 ES6 > ES5 转译器使用的技术捕获生成器的状态非常相似。