一、for-of循环
(1)for循环的疑问
起初我们如何遍历数组中的元素呢?20年前JavaScript刚萌生时,你可能这样实现数组遍历:
var myArray = ['2','3','aaa'];
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
自ES5正式发布后,你可以使用内建的forEach方法来遍历数组:
myArray.forEach(function (value) {
console.log(value);
});
使用forEach的这段代码看起来更加简洁,但这种方法也有一个小缺陷:你不能使用break语句中断循环,也不能使用return语句返回到外层函数。
是不是呢?你们可以进行相关的验证,同时呢它在浏览器兼容与支持方面并不理想,所以我们会想起for循环来。
当然,如果只用for循环的语法来遍历数组元素也很不错,就如上面的第一个例子。
那么,你一定想尝试一下for-in循环:
for (var index in myArray) { // 千万别这样做
console.log(myArray[index]);
}
这绝对是一个糟糕的选择,为什么呢?
> 1.在这段代码中,赋给index的值不是实际的数字,而是字符串“0”、“1”、“2”,而你呢?此时很可能在无意之间需要进行相关计算,但这是这不是数值的计算了,而是字符串算数计算,例如:“2”+1 == “21”,这给编码过程带来极大的不便。
> 2.作用于数组的for-in循环体除了遍历数组元素外,还会遍历自定义属性。举个例子,如果你的数组中有一个可枚举 myArray.name,循环将额外执行一次,遍历到名为“name”的索引。就连数组原型链上的属性都能被访问到。
> 3.最让人震惊的是,在某些情况下,这段代码可能按照随机顺序遍历数组元素。
**> 4.简而言之,for-in是为普通对象设计的,你可以遍历得到字符串类型的键,因此不适用于数组遍历。 **
其实啰里啰唆说了这么多你就记住 一句话:for-in是用于遍历对象的,普通for循环和forEach是遍历普通数组的。 如果有天你想要遍历对象又要得到它的键值,这里举例说明:
var obj = {
'name': '言墨儿',
'age': 21
};
console.log(Object.keys(obj)) // 返回的是一个数组
// 不推荐for-in
for (var val in Object.keys(obj)) {
console.log(val); // 获得键值key-字符串
console.log(Object.keys(obj)[val]); // name、age-(获得键值key对应的数据)
}
// 推荐for循环
for (var i = 0; i < Object.keys(obj).length; i++) {
console.log(i); // 获得键值key-数字
console.log(Object.keys(obj)[i]); // name、age-(获得键值key对应的数据)
}
(2)强大的for-of循环
为了解决这些问题所以新的循环方式在ES6中出现了,当然ES6不会破坏你已经写好的JS代码。目前,成千上万的Web网站依赖for-in循环,其中一些网站甚至将其用于数组遍历。而如果想通过修正for-in循环增加数组遍历支持会让这一切变得更加混乱,因此,标准委员会在ES6中增加了一种新的循环语法来解决目前的问题。
就像这样:
// for-of循环支持数组遍历和大多数类数组对象
var myArray = ['2','3','aaa']
for (var value of myArray) {
console.log(value); // 2,3,aaa
}
是的,与之前的内建方法相比,这种循环方式看起来是否有些眼熟?那好,我们将要探究一下for-of循环的外表下隐藏着哪些强大的功能。现在,你只需记住:
1.这是最简洁、最直接的遍历数组元素的语法
2.这个方法避开了for-in循环的所有缺陷
3.与forEach()不同的是,它可以正确响应break、continue和return语句
表面看来*for-in*循环用来遍历对象属性,for-of循环用来遍历数据—例如数组中的值。但是,for-of不仅如此!
for-of循环也可以遍历其它的集合,for-of循环不仅支持数组,还支持大多数类数组对象,例如DOM NodeList对象。同时,for-of循环也支持字符串遍历,它将字符串视为一系列的Unicode字符来进行遍历:
for (var chr of "yanmoer") {
console.log (chr); // y,a,n,m,o,e,r
}
它同样支持Map和Set对象遍历。
很多人也许没听说过Map和Set对象。他们是ES6中新增的类型。不知道也没有关系,我将在后面简单讲解这两个新的类型,如果你还有兴趣,你可以点击我存放的连接。
如果你曾在其它语言中使用过Map和Set对象,你会发现ES6中的并无太大出入。
其实讲简单点就是Set对象可以自动排除重复项:
// 基于单词数组创建一个set对象
var words = ['yanmoer','qinni','yanmoer']
var uniqueWords = new Set(words); // 如果成为Set对象,输出结果会去掉重复项
console.log(uniqueWords); // Set {"yanmoer", "qinni"}
// 生成Set对象后,你可以轻松遍历它所包含的内容:
for (var word of uniqueWords) {
console.log(word); // yanmoer,qinni
}
Set对象还有一些方法,这里不一一列举了,详情请见官网api:Set 对象
Map 对象则稍有不同:内含的数据由键值对组成,所以你需要使用解构(destructuring)来将键值对拆解为两个独立的变量,例子说明:
var phoneBookMap = [['name','yanmo'],['age','22']]; // Map对象就是这样的
for (var [key, value] of phoneBookMap) { // [key, value]就是解构
console.log(key + " : " + value); //'name' : 'yanmo','age' : '22'
}
其实Map对象就是简单的键/值映射。其中键和值可以是任意值(对象或者原始值)。关于Map对象的方法,也是详情请见官网api:Map 对象
解构也是ES6的新特性,简单说来解构赋值就是允许你使用类似数组或对象字面量的语法将数组和对象的属性赋给各种变量,我将在后续的文章中进行讲解。看来我应该记录这些优秀的问题,未来再进行相关新内容的一一剖析。
现在,你只需记住:未来的JS可以使用一些新型的集合类,甚至会有更多的类型陆续诞生,而for-of就是为遍历所有这些集合特别设计的循环语句。
for-of循环不支持普通对象,你应当用用for-in循环(这也是它的本职工作)或内建的Object.keys()方法:
// 向控制台输出对象的可枚举属性
var someObject = {
classA: 'textColor',
classB: 'textSize',
isA: false
}
for (var key of Object.keys(someObject)) {
console.log(key + ": " + someObject[key]); // classA: 'textColor',classB: 'textSize',isA: false
}
二、深入理解
“能工摹形,巧匠窃意。”——巴勃罗•毕卡索
前言
结合我的实践和运用,ES6及以后ES的后继者都会坚持这样一个规律:凡是新加入的特性,势必已在其它语言中得到强有力的实用性证明。
举个例子,新加入的for-of循环像极了C++、Java、C#以及Python中的循环语句。与它们一样,这里的for-of循环支持语言和标准库中提供的几种不同的数据结构。它同样也是这门语言中的一个扩展点(关于扩展点,建议参考 1. 浅析扩展点 2. What are extensions and extension points?,连接给你们了有兴趣了可以研究,没兴趣了也没关系,继续)。
正如其它语言中的for/foreach语句一样,for-of循环语句通过方法调用来遍历各种集合。数组、Maps对象、Sets对象以及其它在我们讨论的对象有一个共同点,它们都有一个迭代器方法。
你可以给任意类型的对象添加迭代器方法。
当你为对象添加myObject.toString()方法后,就可以将对象转化为字符串,同样地,当你向任意对象添加myObjectSymbol.iterator方法,就可以遍历这个对象了。你们一定很疑问那个[Symbol.iterator]语法是什么情况,这段代码到底做了什么呢?这里通过Symbol处理了一下方法的名称,ES标准委员会可以把这个方法命名为.iterator()方法,但是如果你的代码中的对象可能也有一些.iterator()方法,这不是最好的解决方法。于是在ES6标准中使用symbol来作为方法名,而不是使用字符串。同时,Symbols也是ES6中的新类型。
现在,你需要记住,基于新标准,你可以定义一个全新的symbol,就像Symbol.iterator,如此一来可以保证不与任何已有代码产生冲突。这样做的代价是,这段代码的语法看起来会略显生硬,但是这微乎其微代价却可以为你带来如此多的新特性和新功能,并且你所做的这一切可以完美地向后兼容。
所有拥有Symbol.iterator的对象被称为可迭代的。对于ES来说可迭代对象的概念几乎贯穿于整门语言之中,不仅是for-of循环,还有Map和Set构造函数、解构赋值,以及新的展开操作符。
如果你还看不明白,也没关系,下面我会进行通俗易懂的说明。
(1)迭代器对象
现在,你将无须亲自从零开始实现一个对象迭代器,我会在以后的文章详细讲解,在这里我们只简单了解一下迭代器:
for-of循环首先调用集合的Symbol.iterator方法,紧接着返回一个新的迭代器对象。迭代器对象可以是任意具有.next()方法的对象;for-of循环将重复调用这个方法,每次循环调用一次。
举个例子,这段代码是我能想出来的最简单的迭代器,这里我们不添加for-of循环进行控制台的输出:
// 对象迭代
var zeroesForeverIterator = {
[Symbol.iterator]: function () { // 简而言之只是一个方法
return this;
},
next: function () { // next
return {done: false, value: 0};
}
};
console.log(zeroesForeverIterator.next()); // Object {done: false, value: 0}
console.log(zeroesForeverIterator[Symbol.iterator]()); // 获得是zeroesForeverIterator我们自定义的对象
这里附上控制台输出截图:
如果我们加上for-of循环:
var zeroesForeverIterator = {
[Symbol.iterator]: function () { // 简而言之只是一个方法
return this;
},
next: function () { // next
return {done: false, value: 0};
}
};
console.log(zeroesForeverIterator.next()); // Object {done: false, value: 0}
console.log(zeroesForeverIterator[Symbol.iterator]()); // Object {}
for (var value of zeroesForeverIterator) {
console.log(value); // 0
}
这里附上控制台输出截图:
以及我浏览器崩溃的截图:
相信大家也在网上其他地方看过,每当解释这里都是乱说一通,到最后谁也不明白,经过我的叙述我想到这里大家也都了解了有些眉目,其实只要你细心,我们会发现如果我们加上for-of循环,for-of循环每一次调用.next()方法,它都返回相同的结果,其实返回给for-of循环的结果有两种可能:
1. 我们尚未完成迭代
2. 下一个值为0。这意味着for (var value of zeroesForeverIterator){}将会是一个无限循环。当然,一般来说迭代器不会如此简单。
这个迭代器的设计,以及它的.done和.value属性,从表面上看与其它语言中的迭代器不太一样。在Java中,迭代器有分离的.hasNext() 和.next()方法。在Python中,他们只有一个.next() 方法,当没有更多值时抛出StopIteration异常。
但是所有这三种设计从根本上讲都返回了相同的信息。
迭代器对象也可以实现可选的.return()和.throw(exc)方法。如果for-of循环过早退出会调用.return()方法,异常、break语句或return语句均可触发过早退出。如果迭代器需要执行一些清洁或释放资源的操作,可以在.return()方法中实现。大多数迭代器方法无须实现这一方法。.throw(exc)方法的使用场景就更特殊了:for-of循环永远不会调用它。但是我们还是会在以后更详细地讲解它的作用。
现在我们已了解所有细节,可以写一个简单的for-of循环然后按照下面的方法调用重写被迭代的对象。
首先是for-of循环:
for (var val of obj) {
// 一些语句
}
然后是一个使用以下方法和少许临时变量实现的与之前大致相当的示例:
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
val = $result.value;
一些语句
$result = $iterator.next();
}
这段代码没有展示.return()方法是如何处理的,我们可以添加这部分代码,但我认为这对于我们正在讲解的内容来说过于复杂了。for-of循环用起来很简单,但是其背后有着非常复杂的机制。
说到这 接下来就到了不得不说的生成器
(2)生成器
ES6生成器(Generators)简介
什么是生成器?不懂得人一定会疑问,首先我们从一个示例开始:
function* quips(name) {
yield "你好 " + name + "!";
yield "希望你能喜欢这篇详细介绍ES6的文章";
if (name.startsWith("X")) {
yield "你的名字" + name + "真他妈吊";
}
yield "我们下次再见!";
}
var iter = quips("读者");
console.log(iter) // [object Generator]
console.log(iter.next()) // Object { value: "你好 jorendorff!", done: false }
console.log(iter.next())// Object { value: "希望你能喜欢这篇介绍ES6的译文", done: false }
console.log(iter.next()) // Object { value: "我们下次再见!", done: false }
console.log(iter.next()) / Object { value: undefined, done: true }
控制台输出截图:
这是一只会说话的猫,这段代码很可能代表着当今互联网上最重要的一类应用。(因为你可以发现,这可以控制一个流程)。如果你没有看懂,下面我会进行详解。
这段代码看起来很像一个函数,我们称之为生成器函数,它与普通函数有很多共同点,但是二者有如下区别:
1. 普通函数使用function声明,而生成器函数使用function*声明,function*只是一种写法罢了。
2. 在生成器函数内部,有一种类似return的语法:关键字yield。二者的区别是,普通函数只可以return一次,而生成器函数可以yield多次(当然也可以只yield一次)。在生成器的执行过程中,遇到yield表达式立即暂停,后续可使用例如.next()恢复执行状态。
这就是普通函数和生成器函数之间最大的区别,普通函数不能自暂停,生成器函数可以。生成器做了什么?
当你在我上面的例子中调用quips()生成器函数时发生了什么,那么生成器他就做了什么,写的这么详细,我想大家应该能看懂。
ES6生成器(Generators)案例详解说明
1.我们大概已经习惯了普通函数的使用方式,当你调用它们时,它们立即开始运行,直到遇到return或抛出异常时才退出执行,作为JS程序员你一定深谙此道。
生成器调用看起来非常类似:quips(“jorendorff”)。但是,当你调用一个生成器时,它并非立即执行,而是返回一个已暂停的生成器对象(上述实例代码中的iter)。你可将这个生成器对象视为一次函数调用,只不过立即冻结了,即在你调用生成器,并且还没有使用过.next()方法前,它就好像恰好在生成器函数的最顶端的第一行代码之前冻结了,暂停在那里。
每当你调用生成器对象的.next()方法时,函数调用将其自身解冻并一直运行到下一个yield表达式,再次暂停。这也是在上述代码中我们每次都调用iter.next()的原因,我们获得了quips()函数体中yield表达式生成的不同的字符串值。
当我们调用最后一个iter.next()时,我们最终抵达生成器函数的末尾,所以返回结果中done的值为true。抵达函数的末尾意味着没有返回值,所以返回结果中value的值为undefined。(注:你可以借此判断是不是抵达生成器函数的末尾)
现在你也可以尝试在循环中加入一个yield,看看会发生什么?
ES6生成器(Generators)深入
如果用专业术语描述,每当生成器执行yields语句,生成器的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置)被移出堆栈。然而,生成器对象保留了对这个堆栈结构的引用(备份),所以稍后调用.next()可以重新激活堆栈结构并且继续执行。
…
值得特别一提的是,生成器不是线程,在支持线程的语言中,多段代码可以同时运行,通通常导致竞态条件和非确定性,不过同时也带来不错的性能。生成器则完全不同。当生成器运行时,它和调用者处于同一线程中,拥有确定的连续执行顺序,永不并发,(即类似同步执行)。与系统线程不同的是,生成器只有在其函数体内标记为yield的点才会暂停。
现在,我们了解了生成器的原理,领略过生成器的运行、暂停恢复运行的不同状态。那么,这些奇怪的功能究竟有何用处?
(3)生成器是迭代器!生成器与迭代器
上面我大概讲了ES6的迭代器的概念和小例子,它是ES6中独立的内建类,同时也是语言的一个扩展点,通过实现[Symbol.iterator]()和.next()两个方法你就可以创建自定义迭代器。
注:记住Symbol.iterator和.next()两个方法就可以创建自定义迭代器
生成器代替迭代器
实现一个接口不是一桩小事,我们一起实现一个迭代器。举个例子,我们创建一个简单的名为range迭代器,它可以简单地将两个数字之间的所有数相加。在这里我们使用ES6的类的解决方案(如果不清楚语法细节,无须担心,我也将在以后的文章中为你讲解):
代码:
// 实现一个迭代器:简单地将两个数字之间的所有数相加
// 使用ES6的类的解决方案
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};
}
}
}
// 返回一个新的迭代器,可以从start到stop计数。
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log("ES6的类" + value); // ES6的类0,ES6的类1,ES6的类2
}
这里的实现类似Java或Swift中的迭代器,不是很糟糕,但也不是完全没有问题。我们很难说清这段代码中是否有bug,这段代码看起来完全不像我们试图模仿的传统for (;;)循环,迭代器协议迫使我们拆解掉循环部分。此时此刻你对迭代器可能尚无感觉,他们用起来很好,但看起来有些难以实现和理解。
你大概不会为了使迭代器更易于构建从而建议我们为JS语言引入一个离奇古怪又野蛮的新型控制流结构,但是既然我们有生成器,并且包含Symbol.iterator和.next()两个方法就可以创建自定义迭代器,那么我们是否可以在这里应用生成器来解决问题呢?一起尝试一下:
// 生成器解决:
var oldValue = 0
for (var value of range1(0, 3)) {
console.log("生成器" + value); // 生成器0,生成器1,生成器2
console.log(oldValue + value); // 0,1,3
oldValue = value;
}
function* range1(start, stop) {
for (var i = start; i < stop; i++)
yield i;
}
以上简便代码实现的生成器完全可以替代之前引入了一整个RangeIterator类的代码的实现。可行的原因是:生成器是迭代器。因为所有的生成器都有内建.next()和Symbol.iterator方法的实现。所以你只须编写循环部分的行为。而不再需要写Symbol.iterator和.next()两个方法来创建自定义迭代器。
生成器的最大效力
我们都非常讨厌被迫用古怪语态写一封很长的邮件,不借助生成器实现迭代器的过程与之类似,令人痛苦不堪。当你的语言不再简练,说出的话就会变得难以理解。上面RangeIterator类的实现代码很长并且非常奇怪,因为你需要在不借助循环语法的前提下为它添加循环功能的描述。所以生成器是最好的解决方案!
- 我们如何发挥作为迭代器的生成器所产生的最大效力?
- 使任意对象可迭代(上面关于迭代器的例子,为对象添加Symbol.iterator和.next()方法)。编写生成器函数遍历这个对象,运行时yield每一个值。然后将这个生成器函数作为这个对象的[Symbol.iterator]方法。
简化数组构建函数。假设你有一个函数,每次调用的时候返回一个数组结果。就像这样:
// 拆分一维数组icons变为二维
// 普通实现
function splitIntoRows(icons, rowLength) {
var rows = [];
for (var i = 0; i < icons.length; i += rowLength) {
rows.push(icons.slice(i, i + rowLength));
}
return rows;
}
var icons = [1,2,3,4,5,6,7,8,9,0]
console.log(JSON.stringify(splitIntoRows(icons, 2))); // [[1,2],[3,4],[5,6],[7,8],[9,0]]
// 生成器创建
function* splitInto(icons, rowLength) {
for (var i = 0; i < icons.length; i += rowLength) {
yield icons.slice(i, i + rowLength);
}
}
var str = [1,2,3,4,5,6,7,8,9,0]
for (var value of splitInto(str, 2)) {
console.log("生成器创建数组" + value); // 生成器创建数组1,2 生成器创建数组3,4...生成器创建数组9,0
}
console.log(splitInto(str, 2).next()); // Object {value: Array[2], done: false}
行为上唯一的不同是,传统写法立即计算所有结果并返回一个数组类型的结果,使用生成器则返回一个迭代器,每次根据需要逐一地计算结果。
你一定会疑问,这可以用来做什么?
- 获取异常尺寸的结果。你无法构建一个无限大的数组,但是你可以返回一个可以生成一个永无止境的序列的生成器,每次调用可以从中取任意数量的值。
- 重构复杂循环。你是否写过又丑又大的函数?你是否愿意将其拆分为两个更简单的部分?现在,你的重构工具箱里有了新的利刃——生成器。当你面对一个复杂的循环时,你可以拆分出生成数据的代码,将其转换为独立的生成器函数,然后使用for(var
data of myNewGenerator(args))遍历我们所需的数据。- 构建与迭代相关的工具。ES6不提供用来过滤、映射以及针对任意可迭代数据集进行特殊操作的扩展库。借助生成器,我们只须写几行代码就可以实现类似的工具。
举个例子,假设你需要一个等效于Array.prototype.filter(filter()方法使用指定的函数测试所有元素,并创建一个包含所有通过测试的元素的新数组。),并且支持DOM NodeLists的方法,可以这样写: (Array.prototype.filter)
// 过滤数组中每一项对象的id属性是数字
// 定义数组
var arr = [
{ id: 15 },
{ id: -1 },
{ id: 0 },
{ id: 3 },
{ id: 12.2 },
{ },
{ id: null },
{ id: NaN },
{ id: 'undefined' }
];
var invalidEntries = 0; // 用来记录无效条目数
function isNumber(obj) { // 判断是不是数字
return obj!== undefined && typeof(obj) === 'number' && !isNaN(obj);
}
function filterByID(item) { // 过滤id,是数字的返回true,否则返回false
if (isNumber(item.id)) {
return true;
}
invalidEntries++;
return false;
}
var arrByID = arr.filter(filterByID);
console.log('过滤后的数组\n', arrByID); // 过滤后的数组---[{ id: 15 }, { id: -1 }, { id: 0 }, { id: 3 }, { id: 12.2 }]
console.log('无效条目数 = ', invalidEntries); // 无效条目数 = 4
借助生成器可以非常轻松地实现自定义迭代器,记住,迭代器贯穿ES6的始终,它是数据和循环的新标准。
生成器和异步代码及同步
异步API通常需要一个回调函数,这意味着你需要为每一次任务执行编写额外的异步函数。异步API拥有错误处理规则,不支持异常处理。不同的API有不同的规则,大多数的错误规则是默认的;在有些API里,甚至连成功提示都是默认的。
生成器为你提供了避免异步代码问题的新思路。
实验性的Q.async()尝试结合promises使用生成器产生异步代码的等效同步代码。(async 函数就是 Generator 函数的语法糖,async 函数很多人认为它是异步操作的终极解决方案。)举个例子:
// 制造一些噪音的同步代码。
function makeNoise() {
shake();
rattle();
roll();
}
// 制造一些噪音的异步代码。
// 返回一个Promise对象
// 当我们制造完噪音的时候会变为resolved
function makeNoise_async() {
// 注意目前浏览器都只支持ES5,所以该代码会在浏览器上报错
return Q.async(function* () {
yield shake_async();
yield rattle_async();
yield roll_async();
});
}
// Q.async()尝试结合promises
var eventualAdd = Q.async(function* (oneP, twoP) {
var one = yield oneP;
var two = yield twoP;
return one + two;
});
eventualAdd(eventualOne, eventualTwo).then(function (three) {
three === 3;
});
二者主要的区别是,异步版本必须在每次调用异步函数的地方添加yield关键字。
要玩Q.async()请记得引入,并且在支持ES6转换为ES5的编辑器中编程。这里附上相关链接:Q.async()和q.js,以及async 函数的含义和用法
在Q.async版本中添加一个类似if语句的判断或try/catch块,如同向同步版本中添加类似功能一样简单。与其它异步代码编写方法相比,这种方法更自然,不像是学一门新语言一样辛苦。
当你已经看到这里,如果需要更详细的,你可以试着阅读来自James Long这个老外的更深入地讲解生成器的文章。
生成器为我们提供了一个新的异步编程模型思路,这种方法更适合人类的大脑。此外,更好的语法或许会有帮助,ES7中有一个有关异步函数的提案,它基于promises和生成器构建,并从C#相似的特性中汲取了大量灵感。
如何应用这些疯狂的新特性?
在服务器端,现在你可以在io.js中使用ES6(在Node中你需要使用–harmony这个命令行选项)。
在浏览器端,到目前为止只有Firefox 27+和Chrome 39+支持了ES6生成器。如果要在web端使用生成器,你需要使用Babel或Traceur来将你的ES6代码转译为Web友好的ES5。
起初,JS中的生成器由Brendan Eich实现,他的设计参考了Python生成器,而此外Python生成器则受到Icon的启发。他们早在2006年就在Firefox
2.0中移植了相关代码。但是,标准化的道路崎岖不平,相关语法和行为都在原先的基础上有所改动。Firefox和Chrome中的ES6生成器都是由编译器hacker
Andy Wingo实现的。
结语
生成器还有更多未提及的特性,例如:.throw()和.return()方法、可选参数.next()、yield*表达式语法。由于行文过长,估计各位观众老爷们已然疲乏,剩下的干货择机为大家献上。
转载简书作者:侬姝沁儿
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。