共计 5054 个字符,预计需要花费 13 分钟才能阅读完成。
介绍 ES6 Generators
什么是 Generators(生成器函数)?让咱们先来看看一个例子。
function* quips(name) {
yield "hello" + name + "!";
yield "i hope you are enjoying the blog posts";
if (name.startsWith("X")) {yield "it's cool how your name starts with X, " + name;}
yield "see you later!";
}
这是一只会谈话的猫的一些代码,可能是当今互联网上最重要的一种利用。它看起来有点像一个函数,对吗?这被称为生成器 - 函数,它与函数有很多共同之处。但你马上就能看到两个不同之处。
- 一般函数以 function 结尾,生成器函数以function* 结尾
- 在生成器函数中,yield是一个关键字,语法看起来像return。不同的是,函数(甚至是生成器函数)只能返回一次,而生成器函数能够“yield”任何次数。yield 表达式暂停了生成器的执行,同时它能够在当前再次复原。
Generators 能够做什么
当你调用生成器 - 函数 quips()时会产生什么?
> var iter = quips("jorendorff");
[object Generator]
> iter.next()
{value: "hello jorendorff!", done: false}
> iter.next()
{value: "i hope you are enjoying the blog posts", done: false}
> iter.next()
{value: "see you later!", done: false}
> iter.next()
{value: undefined, done: true}
你可能曾经十分习惯于一般函数和它们的行为形式。当你调用它们时,它们会立刻开始运行,并始终运行到返回或抛出异样。所有这些对任何 JS 程序员来说都是第二天性。调用一个生成器看起来也是一样的:quips(“jorendorff”)。
然而当你调用一个生成器时,它还没有开始运行。相同,它返回一个暂停的 Generator 对象 iter(在下面的例子中调用)。你能够把这个 Generator 对象看作是一个函数调用,在调用前被解冻。具体来说,它被解冻在生成器函数的顶端,就在运行其第一行代码之前。每次你调用 Generator 对象的办法.next() 时,函数调用都会自我冻结,并运行到下一个 yield 表达式为止。这就是为什么咱们每次调用下面的 iter.next() 办法,都会失去一个不同的字符串值。这些都是由函数 quips() 中产生的值。在最初一次 iter.next() 调用中,咱们终于达到了生成器 - 函数的起点,所以后果的字段是。达到一个函数的起点就像返回一样,这就是为什么后果的字段是 {value: undefined, done: true}
当初可能是一个好时机,回到会谈话的猫的演示页面,真正地玩一玩代码。试着把 yield 放在一个循环外面。会产生什么?从技术上讲,每次 Generator 执行 yield 时,它的堆栈 – 局部变量、参数、长期值以及以后在 Generator 主体中的执行地位 – 都会从堆栈中删除。然而,Generator 对象会保留对这个堆栈框架的援用(或正本),以便当前 .next() 调用能够从新激活它并继续执行。
值得指出的是,Generator 不是线程 。在有线程的语言中,多段代码能够同时运行,通常会导致比赛条件、非确定性和苦涩的性能。Generator 则齐全不是这样的。当一个 Generator 运行时,它与调用者在同一个线程中运行。执行的程序是程序的、确定的,而不是并发的。与零碎线程不同,Generator 只在其函数体中表明的yield 点上暂停运行。
好了。咱们晓得 Generator 是什么。咱们曾经看到了一个 Generator 的运行,暂停本人,而后复原执行。当初有个大问题。这种奇怪的能力怎么可能有用?
Generators 就是迭代器(Generators are iterators)
ES6 迭代器不仅仅是一个繁多的内置类。它们是该语言的一个扩大点。你能够通过实现两个办法 [Symbol.iterator]() 和next()来创立你本人的迭代器。然而实现一个接口至多要做一点工作。让咱们看看迭代器的实现在实践中是什么样的。作为一个例子,让咱们做一个简略的迭代器range for (;;),它只是从一个数字到另一个数字进行计数,就像一个老式的 C 循环一样。
// This should "ding" three times
for (var value of range(0, 3)) {alert("Ding! at floor #" + value);
}
这里有一个解决方案,应用 ES6 类class。
class RangeIterator {constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this;}
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
} else {return {done: true, value: undefined};
}
}
}
// Return a new iterator that counts up from 'start' to 'stop'.
function range(start, stop) {return new RangeIterator(start, stop);
}
代码示例
这就是在 Java 或 Swift 中实现迭代器的状况。这并不坏。但也不齐全是微不足道的。这段代码里有什么谬误吗?这可不好说。它看起来齐全不像咱们在这里试图模拟的原始循环:for (;;),迭代器协定迫使咱们拆除了循环。在这一点上,你可能对迭代器感到有点淡漠。它们可能很好用,但仿佛很难实现。
你可能不会想倡议咱们在 JS 语言中引入一个疯狂的、令人费解的新控制流构造,只是为了使迭代器更容易构建。但既然咱们有生成器Generator,咱们能在这里应用它们吗?让咱们试试吧。
function* range(start, stop) {for (var i = start; i < stop; i++)
yield i;
}
代码示例
下面的 4 行 range() 代码能够间接代替以前的 23 行实现,包含整个类 RangeIterator。 就是因为 Generator 是迭代器,所以这所有才是可能的。所有的生成器都有一个内置的 next() 和[Symbol.iterator]()的实现。你只需写出循环的行为。
在没有 Generator 的状况下实现迭代器,就像被迫齐全用被动语态来写一封长邮件。当简略地说出你的意思不是一个选项时,你最终说的货色可能会变得相当简单。” 我的意思是,我的意思是,我必须在不应用循环语法的状况下形容一个循环的性能,所以 RangeIterator 又长又奇怪。而Generator 就是答案。
咱们还能够如何利用生成器作为迭代器的能力呢?
- 让任何对象都能够迭代。只需写一个 Generator 函数来遍历,在遍历时产生(yield)每个值。而后把这个生成器函数装置为 this 对象的 [Symbol.iterator] 办法。
-
简化建数组函数。假如你有一个函数,每次调用都会返回一个数组的后果,就像上面这个函数:
// 将一维数组 '图标' 切分成长度为 'rowLength' 的数组 function splitIntoRows(icons, rowLength) {var rows = []; for (var i = 0; i < icons.length; i += rowLength) {rows.push(icons.slice(i, i + rowLength)); } return rows; }
应用 Generator 会让这种代码更短一些。
function* splitIntoRows(icons, rowLength) {for (var i = 0; i < icons.length; i += rowLength) {yield icons.slice(i, i + rowLength); } }
执行时惟一区别是,它不是一次性计算所有的后果并返回一个数组,而是返回一个迭代器,而后依据须要一一计算结果。
- 异样大小的后果。你不可能建设一个有限的数组。然而你能够返回一个 Generator,生成一个无尽的序列,每个调用者能够从其中提取他们须要的任何数量的值。
- 重构简单的循环。你有一个微小的俊俏的函数吗?你想把它分解成两个更简略的局部吗?Generator 是增加到你的重构工具箱中的一把新刀。当你面对一个简单的循环时,你能够把代码中产生数据的局部合成进去,把它变成一个独自的生成器 - 函数。而后将循环改为:for (var data of myNewGenerator(args))
-
解决可迭代数据的工具。ES6 并没有提供一个扩大库,用于过滤、映射,以及个别状况下对任意的可迭代数据集进行任意的解决。然而 Generator 对于构建你所须要的工具来说是十分棒的,只须要几行代码。例如,假如你须要一个新的在 DOM NodeLists 上遍历的办法,而不仅仅是 Arrays。小菜一碟:创立 Array.prototype.filter
function* filter(test, iterable) {for (var item of iterable) {if (test(item)) yield item; } }
那么 Generator 是否有用呢?当然,它们是实现自定义迭代器的一种惊人的简略办法,而且迭代器是整个 ES6 的数据和循环的新规范。但这并不是 Generator 的全副性能。这甚至可能不是它们所做的最重要的事件。
生成器与异步代码(Generators and asynchronous code)
上面是我前段时间写的一些 JS 代码。
};
})
});
});
});
});
兴许你曾经在本人的代码中看到了这样的货色。异步 API 通常须要一个回调,这意味着每次你做什么都要写一个额定的匿名函数。因而,如果你有一点代码做三件事,而不是三行代码,你就会看到三个缩进档次的代码。上面是我写的一些更多的 JS 代码。
}).on('close', function () {done(undefined, undefined);
}).on('error', function (error) {done(error);
});
异步 API 有错误处理常规,而不是异样。不同的 API 有不同的约定。在大多数 API 中,默认状况下,谬误会被默默地放弃。在一些 API 中,即便是一般的胜利实现也是默认放弃的。直到现在,这些问题都是咱们为异步编程付出的代价。咱们曾经承受了这样的事实:异步代码看起来并不像相应的同步代码那样丑陋和简略。
Generator 提供了新的心愿:咱们不用再写那样俊俏的代码。
Q.async()是一个实验性的尝试,它应用 Generators 与 Promises 来产生相似于相应同步代码的异步代码。比如说:
// Synchronous code to make some noise.
function makeNoise() {shake();
rattle();
roll();}
// Asynchronous code to make some noise.
// Returns a Promise object that becomes resolved
// when we're done making noise.
function makeNoise_async() {return Q.async(function* () {yield shake_async();
yield rattle_async();
yield roll_async();});
}
次要的区别是,异步版本必须在调用异步函数的每个中央增加关键字yield。在该版本中增加像语句 if 或 try/catch 块这样的代码,就像在一般的同步版本中增加它一样。与其余编写异步代码的形式相比,这感觉不像是在学习一种全新的语言。(延长浏览)
因而,Generator 为一种新的异步编程模型指明了方向,它仿佛更适宜人类的大脑。这项工作正在进行中。在其余方面,更好的语法可能会有帮忙。一项对于异步函数的倡议,建设在 Promises 和 Generators 的根底上,并从 C# 的相似性能中取得灵感,已被提上 ES7 的议程。