ES6学习笔记7--迭代器与生成器

  |  

前言

继续ES6的学习,这部分关于迭代器与生成器


8



循环的问题

循环是一个语言必不可少的问题,在JS中最简单的循环写法莫过于for循环

1
2
3
4
var colors = ["red", "green", "blue"];
for(var i = 0, len = colors.length; i < len; i++) {
console.log(colors[i]);
}

这里采用for循环的标准写法,采用变量i来追踪colors数组中的位置索引。

但是for循环有极大的局限性,当循环开始多层嵌套,需要追踪的变量数量开始增加,复杂性就会开始增加,随之而来的就是出错的可能性。所以迭代器出现了

何为迭代器

迭代器是被设计专用于迭代的对象,带有特定接口。所有的迭代器对象都拥有next()方法,会返回一个结果对象。该结果对象有两个属性:

  • 对应下一个值的value
  • 布尔类型的done,其值为true时表示没有更多的值可供使用

迭代器其持有一个指向集合位置的内部指针,每当调用next()方法,迭代器就会返回相应的下一个值。

当在最后一个值返回后再调用next(),返回的done值会是true,并且value属性会视迭代器自身的返回值。该返回值并不是原始数据集的一部分,却会成为相关数据的最后一个片段,或在迭代器未提供返回值的时候使用undefined。

在ES5中我们这样创建一个迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
function createIterator(item) {
var i = 0;
return {
next: function() {
var done = (i >= item.length);
var value = !done ? item[i++] : undefined;
return {
done: done,
value: value
}
}
}
}

createIterator()函数返回一个带有next()方法的对象,每当调用该对象时,会返回items数组的下一个值和done属性。

我们可以这样来使用这个迭代器

1
2
3
4
5
6
var iterator = createIterator([1, 2, 3]);
consoloe.log(iterator.next()); // "{ value: 1, done: false}"
consoloe.log(iterator.next()); // "{ value: 2, done: false}"
consoloe.log(iterator.next()); // "{ value: 3, done: false}"
consoloe.log(iterator.next()); // "{ value: undefined, done: true}"
consoloe.log(iterator.next()); // "{ value: undefined, done: true}"

何为生成器

生成器是能返回一个迭代器的函数,生成器函数由在function关键字之后的一个星号(*)来表示,并能使用新的关键字yield。星号紧跟function关键字或者空出空格都没问题

1
2
3
4
5
6
7
8
9
function *createIterator() {
yield 1;
yield 2;
yield 3;
}
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

yield关键字是ES6新增的,指定了迭代器在被next()方法调用时应当按顺序返回的值。

生成器函数最有意思的地方在与它们会在每个yield语句后停止执行,直到迭代器的next()方法被调用,才会执行下一句。在函数中停止执行的能力是极其强大的,必能引出生成器函数的一些有趣用法

yield关键字可以和值或是表达式一起使用,因此可以通过生成器给迭代器添加项目,而不是机械的列出迭代输出

1
2
3
4
5
function *createIterator(items) {
for (let i = 0, len = items.length; i < len; i++) {
yield items[i];
}
}

注意

yield关键字只能用在生成器内部,用于其它任意位置都是语法错误,即使在生成器内部中的函数内部也不行。这一点yield与return非常相似,在一个被嵌套的函数中无法将值返回给包含它的函数,及无法穿越函数边界。

生成器函数表达式

除了使用函数声明语句可以创建一个生成器外,同样可以使用函数表达式来创建一个生成器,只要在function关键字和圆括号间使用一个星号即可

1
2
3
4
5
let createIterator = function *(items) {
for(let i = 0, len = items.length; i < len; i++) {
yield items[i]
}
}

注意

不能使用箭头函数来创建生成器

生成器对象方法

由于生成器就是函数,因此也可以被添加到对象中,只不过针对不同版本写法不同罢了

在ES5中

1
2
3
4
var o = {
createIterator: function *(items) {
}
}
1
2
3
4
var o = {
*createIterator(items) {
}
}

ES6中的速记法,只要在方法名之前加上一个星号即可

可迭代对象与for-of循环

与迭代器紧密相关的是可迭代对象,可迭代对象是包含Symbol.iterator属性的对象。这个Symbol.iterator知名符号定义了为指定对象返回迭代器的函数。在ES6中,所有的集合对象(数组、Set与Map)以及字符串都是可迭代对象,因此它们都被指定了默认的迭代器。

生成器创建的所有迭代器都是可迭代对象,因为生成器默认就会为SYmbol.iterator属性赋值

for-of循环在循环每次执行时都会调用可迭代对象的next()放啊发,并将结果对象的value值存储在一个变量上。循环过程会持续到结果对象的done属性变为true为止。

1
2
3
4
5
6
7
8
9
function *createIterator(items) {
for(let i = 0, len = items.length; i < len; i++) {
yield items[i];
}
}
let values = createIterator([1, 2, 3, 4, 5]);
for(let num of values) {
console.log(num);
}

这个for-of循环会先调用values数组的Symbol.iterator方法,获取一个迭代器(这个过程由JS引擎后台实现)。接下来调用iterator.next()方法,迭代器结果对象的value属性被读取并存放在num变量

在不可迭代对象、null或undefined上使用for-of语句会抛出错误

访问默认迭代器

你可以使用Symbol.iterator来访问对象上的默认迭代器

1
2
3
4
5
6
let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();
console.log(iterator.next()); // "{value: 1, done: false}"
console.log(iterator.next()); // "{value: 2, done: false}"
console.log(iterator.next()); // "{value: 3, done: false}"
console.log(iterator.next()); // "{value: undefined, done: true}"

这段代码获取了values数组的默认迭代器,并用它来迭代数组中的项。这个过程与使用for-of循环时在后台发生的过程一致。

既然Symbol.iterator指定了默认迭代器,你就可以使用它来检测一个对象是否能进行迭代

1
2
3
4
5
6
7
8
9
function isIterable(object) {
return typeof object[Symbol.iterator] === "function";
}
console.log(isIterable([1, 2, 3])); //true
console.log(isIterable("Hello")); //true
console.log(isIterable(new Map())); //true
console.log(isIterable(new Set())); //true
console.log(isIterable(new WeakMap())); //false
console.log(isIterable(new WeakSet())); //false

for-of循环在执行之前会做类似的检查

创建可迭代对象

开发者自定义对象默认情况下不是可迭代对象,但你可以创建一个包含生成器的Symbol.iterator属性,让他们成为可迭代对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let collection = {
items: [],
*[Symbol.iterator]() {
for (let item of this.items) {
yield item;
}
}
};
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for(let x of collection) {
console.log(x);
}

这个例子首先为对象定义了Symbol.iterator属性,当对象被迭代时就会访问这个属性。属性需要返回一个迭代器,然后对象根据这个迭代器进行迭代。这个例子我们的Symbol.iterator属性被定义为一个生成器,所以访问时可以直接返回一个迭代器。当然我们可以将这个属性定义为普通函数,只要它能返回一个生成器就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: function() {
let i = 0;
return {
next: () => {
let done = (i >= this.length);
let value = this[i];
i++;
return {
done: done,
value: value
}
}
}
}
}

这里我们将Symbol.iterator定义为一个普通的函数,虽然它是个普通的函数,但是同样返回一个对象,而且这对象拥有next属性,next属性是一个函数,返回一个结果对象,这个结果对象拥有done和value属性。显然我们返回的就是一个迭代器,所以我们同样可以将原对象变成一个可迭代对象。

内置的迭代器

迭代器是ES6的一个重要部分,无需为许多内置类型创建额外的迭代器,只有在内置的迭代器无法满足需要时,才有必要创建自定义迭代器。否则完全可以以靠内置的迭代器来完成工作。

集合的迭代器

ES6具有三种集合对象类型:数组、Map与Set。这三种类型都拥有如下的迭代器:

  • entries():返回一个包含键值对的迭代器
  • values():返回一个包含集合中的值的迭代器
  • keys():返回一个包含集合中的键的迭代器

entries()迭代器
entries迭代器会在每次next()被调用时返回一个双项数组,此数组代表了集合中每个元素的键与值:对于数组来说,第一项就是数组的索引;对于Set,第一项也是值;对于Map,第一项就是键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let colors = ["red", "green", "blue"];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for(let entry of colors.entries()) {
console.log(entry);
}
for(let entry of tracking.entries()) {
console.log(entry);
}
for(let entry of data.entries()) {
console.log(entry);
}

values()迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let colors = ["red", "green", "blue"];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "Understanding ES6");
data.set("format", "ebook");
for(let entry of colors.values()) {
console.log(entry);
}
for(let entry of tracking.values()) {
console.log(entry);
}
for(let entry of data.values()) {
console.log(entry);
}

keys()迭代器

keys()得带器返回集合中的每一个键。对于数组来说,它只返回数值类型的键,永不返回数组的其它自由属性;Set的键与值是相同的,因此它的keys()与values()返回了相同的迭代器;对于Map,keys()迭代器返回每个不重复的键。

集合类型的默认迭代器

当for-of循环没有显式指定迭代器时,每种集合类型都有一个默认的迭代器供循环使用。数组与Set的默认迭代器是values(),Map的默认迭代器是entries()

解构与for-of循环

Map默认迭代器的行为有助于在for-of循环中使用解构语法

1
2
3
4
5
6
let data = new Map();
data.set("title", "Understanding Es6");
data.set("format", "ebook");
for(let [key, value] of data) {
console.log(key + "=" + value);
}

这个代码使用迭代器加数组解构来提取Map集合中的值

字符串的迭代器

从ES5发布开始,JS的字符串就变得越来越像数组。例如ES5标准化了字符串的方括号表示法,用于访问其中的字符。不过方括号表示法工作在码元上而非字符串,因此它不能被用于正确的访问双字节字符

1
2
3
4
var message = "A我B" ;
for(let i = 0; i < message.length; i++) {
console.log(message[i]);
}

ES6旨在为Unicode提供完全支持,字符串的默认迭代器就是解决字符串迭代问题的一种尝试。借助字符串默认迭代器就能处理字符而不是码元

1
2
3
4
var message = "A我B";
for(let c of message) {
console.log(c);
}

NodeList的迭代器

文档对象模型(DOM)具有一种NodeList类型,用于表示页面文档中元素的集合。随着默认迭代器被附加到ES6,DOM关于NodeList的规定也包含了一个默认迭代器,其表现方式与数组的默认迭代器一致

1
2
3
4
var divs = document.getElementByTagName("div");
for(let div of divs) {
console.log(div.id);
}

扩展运算符与非数组的可迭代对象

扩展运算符能作用于所有可迭代第项,并且会使用默认迭代器来判断需要使用哪些值。所有的值都可以从迭代器中被读取出来并插入数组,遵循迭代器返回值的顺序。

1
2
3
let map = new Map([["name", "Nocholas"], ["age", 25]]);
let array = [...map];
console.log(array); //[["name", "Nocholas"], ["age", 25]]

由于Map的默认迭代器返回的是键值对的数组,所以最终的结果跟开始传入的参数一致

可以不限次数的在数组字面量中使用扩展运算符,而且可以在任意位置用扩展运算符将可迭代对象的多个项插入数组,这些项在新数组中将会出现在扩展运算符对应的位置

扩展运算符是将可迭代对象转换为数组的最简单方法,可以将字符串转换为包含字符的数组,也能将浏览器中的NodeList对象转换为节点数组

迭代器高级功能

在单纯迭代集合的值之外的任务中,迭代器会显得更加强大

传递参数给迭代器

可以通过next()方法向迭代器传递参数。当一个参数被传递给next()方法时,该参数的值就会代替生成器内部上次调用yield语句的返回值。这种能力对于跟多高级功能(例如异步编程)来说非常重要

1
2
3
4
5
6
7
8
9
10
function *createIterator() {
let first = yield 1;
let second = yield first + 2;
yield second + 3;
}
let iterator = createIterator();
console.log(iterator.next(3)); // "{value: 1, done: false}"
console.log(iterator.next(4)); // "{value: 6, done: false}"
console.log(iterator.next(5)); // "{value: 8, done: false}"
console.log(iterator.next()); // "{value: undefined, done: true}"

程序很简单,但要理解有点复杂。

首先为什么第一个参数3没有起作用,因为对于next的首次调用是个特殊情况,传递给它的任意参数都会被忽略。由于参数的值会代替生成器上次调用yield语句的返回值,而第一次调用并没有上次调用,所以参数无效

接下来,第二次参数4,他会成为上次调用yield的返回值,即原先的let first = yield 1变成了let first = 4,那么接下来的yield first + 2当然就输出了6

参数5同理

在迭代器中抛出错误

能传递给迭代器的不仅是数据,还可以是错误条件。迭代器可以选择实现一个throw()方法,用于指示迭代器应在恢复执行时抛出一个错误。这是对异步编程来说很重要的一个能力, 同时也会增加生成器内部的灵活度,既能模仿返回一个值,也可以模仿抛出错误

1
2
3
4
5
6
7
8
9
function *createIterator() {
let first = yield 1;
let second = yield first + 2;
yield second + 3;
}
let iterator = createIterator();
console.log(iterator.next()); //"{value: 1, done: false}"
console.log(iterator.next(4)); //"{value: 6, done: false}"
console.log(iterator.throw(new Error("Boom"))); //抛出错误

错误的抛出位置和参数传入替代的位置相同

除此之外,我们可以在生成器内部使用try-catch语句来捕捉错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function *createIterator() {
let first = yield 1;
let second;
try {
second = yield first + 2;
} catch (ex) {
second = 6;
}
yield second + 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{value: 1, done: false}"
console.log(iterator.next(4)); // "{value: 6, done: false}"
console.log(iterator.throw(new Error("Boom"))); // "{value: 1, done: false}"
console.log(iterator.next()); // "{value: 1, done: false}"

当错误在生成器内部被捕捉,代码会继续执行到下一个yield处。注意错误发生的时间,如果在不能被捕捉的时候传入错误,还是只会报错。

生成器的Return语句

生成器本质上来说还是一个函数,所以可以在生成器内使用return语句。return语句既可以让生成器提前退出执行,也可以指定在next()方法最后一次调用时的返回值。

1
2
3
4
5
6
7
8
function *createIterator() {
yield 1;
return 2;
yield 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{value: 1, done: false}"
console.log(iterator.next()); // "{value: 2, done: true}"

在生成器内部,return表明所有的处理已完成,因此done属性会被设置为true,如果不指定返回的值,那value属性的值即为undefined。

return语句中指定的任意值只会在结果对象中出现一次,此后value字段就会被重置为undefined。

扩展运算符与for-of循环会忽略return语句所指定的人一直,但是,它们看到done的值为true是,他们就会停止操作,而不会读取对应的value值

生成器委托

在某些情况下我们可以将两个迭代器合并到一个生成器中使用。生成器可以使用星号(*)配合yield这一特殊形式来委托其它的迭代器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function *createNumberIterator() {
yield 1;
yield 2;
}
function *createColorIterator() {
yield "red";
yield "green";
}
function *createCombinedIterator() {
yield *createNumberIterator();
yield *createColorIterator();
yield true;
}
let iterator = createCombinedIterator();
console.log(iterator.next()); // "{value: 1, done: false}"
console.log(iterator.next()); // "{value: 2, done: false}"
console.log(iterator.next()); // "{value: "red", done: false}"
console.log(iterator.next()); // "{value: "green", done: false}"
console.log(iterator.next()); // "{value: true, done: false}"
console.log(iterator.next()); // "{value: undefined, done: true}"

每次对next()的调用都会委托给合适的生成器,直到被委托的生成器生成的迭代器全部清空

生成器委托能进一步使用生成器的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function *createNumberIterator() {
yield 1;
yield 2;
return 3;
}
function *createRepeatingIterator(count) {
for (let i=0; i < count; i++) {
yield "repeat";
}
}
function *createCombinedIterator() {
let result = yield *createNumberIterator();
yield *createRepeatingIterator(result);
}
let iterator = createCombinedIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 2, done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: "repeat", done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

除此之外我们可以在可迭代对象上使用yield *,例如yield * "hello",可迭代对象的默认迭代器或指定迭代器就会被使用

异步任务运行

生成器的强大凸显在异步编程中。生成器在执行过程中能有效地暂停代码操作,因此它能有效进行异步开发

一个简单的任务运行器

生成器能进行异步编程的最核心点在于yield关键字,yield能停止运行,并在重新开始运行前等待next()方法被调用,这样我们就能在没有回调函数的情况下实现异步调用。

首先我们需要编写一个运行函数,可以调用生成器并启动迭代器的函数

1
2
3
4
5
6
7
8
9
10
11
function run(taskDef) {
let task = taskDef(); //创建迭代器
let result = task.next(); //启动任务
function step() { //定义递归函数来保持对next()的调用
if(!result.done) {
result = task.next();
step();
}
}
step(); //开始处理过程
}

来分析这个运行函数,run()函数接受一个任务定义(及一个生成器函数)作为参数,在函数内部调用这个生成器来调用迭代器。通过第一次对next()的调用启动迭代器,并将结果存储。递归函数检查result.done的结果,为false则在递归调用之前调用next()方法。每次调用next()都会把返回的结果保存在result变量上,它总是会被最新的信息所重写。

通过这个运行函数,我们就可以运行一个包含多条yield语句的生成器

1
2
3
4
5
6
7
run(function *() {
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
})

带数据的任务运行

传递数据给任务运行器最简单的方式,就是把yield返回的值传入下一次的next()调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function run(taskDef) {
let task = taskDef();
let result = task.next();
function step() {
if(!result.done) {
result = task.next(result.value);
step();
}
}
step();
}
run(function *() {
let value = yield 1;
console.log(value);
value = yield value + 3;
console.log(value);
})

异步任务运行器

上个例子知识在yield之间来回传递静态数据,但与等待一个异步处理还是有所差距。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function run(taskDef) {
let task = taskDef();
let result = task.next();
function step() {
if(!result.done) {
if(typeof result.value === "function") {
result.value(function(err. data) {
if(err) {
result.task.throw(err);
return;
}
result = task.next(data);
step();
});
} else {
result = task.next(result.value);
step();
}
}
}
step();
}

这个运行函数就很像一个异步执行函数了。当result.value是一个函数时,他会被使用一个回调函数进行调用。回调函数有两个参数,err和data,如果err非空,说明错误发生,throw错误。若不存在错误,data参数将会被传入task.next(),而其调用结果会被重新保存到result中,接下来继续调用step()进行下一步操作。如果result.value值为非函数,他就会被直接传递给next()方法

文章目录
  1. 1. 循环的问题
  2. 2. 何为迭代器
  3. 3. 何为生成器
    1. 3.1. 生成器函数表达式
    2. 3.2. 生成器对象方法
  4. 4. 可迭代对象与for-of循环
    1. 4.1. 访问默认迭代器
    2. 4.2. 创建可迭代对象
  5. 5. 内置的迭代器
    1. 5.1. 集合的迭代器
    2. 5.2. 字符串的迭代器
    3. 5.3. NodeList的迭代器
  6. 6. 扩展运算符与非数组的可迭代对象
  7. 7. 迭代器高级功能
    1. 7.1. 传递参数给迭代器
    2. 7.2. 在迭代器中抛出错误
    3. 7.3. 生成器的Return语句
    4. 7.4. 生成器委托
  8. 8. 异步任务运行
    1. 8.1. 一个简单的任务运行器
    2. 8.2. 带数据的任务运行
    3. 8.3. 异步任务运行器
|