ES6学习(1)

Generators

首先我们来看一示例,第一次看到我是懵逼的

1
2
3
4
5
6
7
8
function* quips(name) {
yield 'Hello' + name + '!';
yield 'Hope you like my blog';
if(name.startsWith('J')){
yield 'your name' + name + 'the first world is J, tha`s cool!';
}
yield 'see you next time';
}

点击这个链接,我们来互动下再继续)

这段代码看起来像是一个函数,我们称之为生成器函数,它与普通函数有很多共同点,但是二者有以下的区别;

  • 普通函数使用function声明,而生成器函数使用function*声明
  • 在生成器函数内部,有一种类似return的语法: 关键字yield,这两者的区别是,普通函数只可以return一次,而生成器函数可以yield一次或者多次。在生成器的执行过程中,遇到yield表达式立即暂停,后续可恢复执行状态。
    很明显普通函数和生成器函数之间的最大区别就是:普通函数不能自暂停,生成器函数可以。

    那么,生成器做了什么?

    当你调用quips()生成器函数时What happend?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    > var iter = quips('JWQ');
    [object Generator]
    > iter.next()
    { varlue: 'Hello JWQ!', done: false }
    > iter.next()
    { value: 'Hope you like my blog', done: false }
    > iter.next()
    { value: 'see you next time', done: false }
    > iter.next()
    { value: undefined, done: true }

你大概已经习惯了普通函数的使用方式,当你调用它们时,它们立即开始运行,直到遇到return或抛出异常时才退出执行,作为JS程序员你一定深谙此道。

生成器调用看起来非常类似:quips(“JWQ”)。但是,当你调用一个生成器时,它并非立即执行,而是返回一个已暂停的生成器对象(上述实例代码中的iter)。你可将这个生成器对象视为一次函数调用,只不过立即冻结了,它恰好在生成器函数的最顶端的第一行代码之前冻结了。

每当你调用生成器对象的.next()方法时,函数调用将其自身解冻并一直运行到下一个yield表达式,再次暂停。

这也是上面得代码中每次都调用iter.next()的原因,我们获得了quips()函数体中yield表达式生成的不同的字符串值。
调用最后一个iter.next()时,我们最终抵达生成函数的末尾,所以返回结果中done的值为true。抵达函数的末尾以为这咩有返回值,所以返回结果中value的职位undefined。

我们在循环中加入一个yield会发什么?

如果用专业术语描述,每当生成器执行yields语句,生成器的堆栈结构(本地变量、参数、临时值、生衡器内部当前的执行位置)被移除堆栈。然而,生成器对象保留了对这个堆栈结构的引用(备份),所以稍后调用.next()可以重新激活堆栈结构并且继续执行。

特别需要一提的是,生成器不是线程,在支持线程的语言中,多段代码可以同时运行,通常导致竞态条件和非确定性,不过同时也带来了不错的性能。生成器则完全不同。当生成器运行时,它和调用者处于同一线程中,拥有确定的连续执行顺序,永不并发。与系统线程不同的是,生成器只有在其和函数体内标记为yield的点才会暂停。

生成器是迭代器

我们知道ES6的迭代器是独立的内建类,同时也是语言的一个扩展点,通过实现[Symbol.iterator]().next()两个方法你就可以创建自定义迭代器。

举个栗子,我们创建一个简单的range迭代器,他可以简单的将两个数字之间的所有数相加。首先是传统的C的循环for(;;):

1
2
3
4
// 这里会弹出5次alert
for (var value of range(0, 5)) {
alert("Yaho~, now we at floor" + value);
}

使用ES6的类解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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, vlaue: undefined };
}
}
}
// 返回一个新的迭代器,可以从start到stop计数
function range (start, stop) {
return new RangeIterator(start, stop);
}

有学过的Java或者Suift的同学不难发现这里的JS和强类型语言十分相似,可能我学了个假的JS :)

我们再来看看使用生成器的方法:

1
2
3
4
5
function* range (start, stop) {
for (var i = start; i < stop; i++){
yield i;
}
}

上面简单的几行代码实现的生成器可以完全替代之前引入的一整个RangeIterator类的20几行的代码。可行的原因是:生成器是迭代器。所有的生成器都有内建.next()[Symbol.iterator]()方法的实现。我们只须编写循环部分的行为代码。

如何发挥作为迭代器的生成器所产生的的最大功效?

  • 使任意对象可迭代。编写生成器函数遍历这个对象,运行时yield每一个值。然后将这个生成器函数作为这个对象的[Symbol.iterator]方法。
  • 简化数组构建函数。假设有一个函数,每次调用的时候返回一个数组结果,like this:
1
2
3
4
5
6
7
8
9
// 拆分一堆数组icons
// 根据长度rowLength
function spltIntoRows(icons, rowLength) {
var rows = [];
for (var i =0; i < icons.length; i += rowLength) {
rows.push(icons.slice(i, i + rowLength));
}
return rows;
}

使用生成器创建的代码相对较短

1
2
3
4
5
function* splitIntoRows(icons, rowLength) {
for (var i = 0; i < icons.length; i += rowLength) {
yield icons.slice(i, i + rowLength);
}
}

行为上唯一的不同是,传统写法立即计算所有结果并返回一个数组类型的结果,使用生成器则返回一个迭代器,每次根据需要逐一地计算结果。

  • 获取异常尺寸的结果。你无法构建一个无限大的数组,但是你可以返回一个可以生成永无止境的序列的生成器,每次调用可以从中取任意数量的值。
  • 重构复杂循环。利用生成器,但你面对一个负责的循环时,你可以拆分出生成数据的代码,将其转换为独立的生成器函数,然后使用for(var data fo myNewGenerator(args))遍历我们所需的数据。
  • 构建与迭代相关的工具。ES6不提供用来过滤、映射以及针对任意可迭代数据数据集进行特殊操作的扩展库。借助生成器,我们只须写几行代码就可以实现类似的工具。

举个栗子:现在你需要一个等效于Array.prototype.file并且支持DOM NodeLists的方法,可以这样写;

1
2
3
4
5
6
7
function* filter(test, iterable) {
for (var item of iterable) {
if (test(item)) {
yield item;
}
}
}

哒啦,借助生成器可以轻松的实现自定义迭代器,我们要牢记一个点:迭代器贯穿Es6的始终,它是数据和循环的新标准。

也许之前你的JS代码是这样的

1
2
3
4
5
6
};
});
});
});
});
});

这是可怕的回调地狱问题,异步API通常需要一个回调函数,这意味着你需要为每一次任务执行编写额外的异步函数。所以如果你有一段代码需要完成三个任务,你将看到类似的三层级缩进的代码,而非简单的三行代码。

进阶后我们会这样写

1
2
3
4
5
)}.on ('close', function () {
done(undefined, undefined);
}).on('error', function (error) {
done(error);
});

异步API拥有错误处理股则,不支持异常处理。不同的API有不同的规则,大多数的错误规则是默认的;在有些API里,甚至连成功提示都是默认的。

这些是到目前为止我们为异步编程所付出的代价,我们正慢慢开始接受异步代码不如等效同步代码美观又简介的事实。

生成器为我们提供了避免以上问题的新思路。
实验性的Q.async()尝试结合promises使用生成器产生异步代码的等效同步代码。举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 制造一些噪音的同步代码。
function makeNoise() {
shake();
rattle();
roll();
}
// 制造一些噪音的异步代码。
// 返回一个Promise对象
// 当我们制造完噪音的时候会变为resolved
function makeNoise_async() {
return Q.async(function* () {
yield shake_async();
yield rattle_async();
yield roll_async();
});
}

二者主要的区别是,异步版本必须在每次调用异步函数的地方添加yield关键字。

在Q.async版本中添加一个类似if语句的判断或try/catch块,如同向同步版本中添加类似功能一样简单。与其它异步代码编写方法相比,这种方法更自然,不像是学一门新语言一样辛苦。
向大家推荐 James Long 的使用JavaScript生成器解决回调的研究

生成器为我们提供了一个新的异步编程模型思路,这种方法更适合人类的大脑。相关工作正在不断展开。此外,更好的语法或许会有帮助,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实现的。这项工作由彭博赞助支持(没听错,就是大名鼎鼎的那个彭博!)。

yield

生成器还有更多未提及的特性,例如:.throw()和.return()方法、可选参数.next()、yield*表达式语法。下回再叙

×

可以的话,我想喝杯咖啡续命

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. Generators
    1. 1.1. 那么,生成器做了什么?
    2. 1.2. 生成器是迭代器
    3. 1.3. 如何发挥作为迭代器的生成器所产生的的最大功效?
  2. 2. 如何应用这些疯狂的新特性?
  3. 3. yield