2019-7-24
第八章 迭代器与生成器
for循环的问题: for循环嵌套使用容易写错循环变量,复杂度变高。
(一) 何为迭代器
迭代器:
- 有一个指向集合位置的内部指针;
- 有next()方法,调用时返回一个包含value属性和done属性的对象;
- 每调用一次next方法,迭代器就会返回相应的下一个值;
- 若在最后一个值被返回后再调用next,所返回的done属性为true,且value属性值为迭代器自身的返回值,即为用return语句明确返回的值,未提供时会返回undefined。
ES5中创建迭代器:
function createIterator(items) {
var i = 0;
return {
next: function() {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;
return {
done: done,
value: value
};
}
};
}
var iterator = createIterator([1, 2, 3]);
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 }"
// for all further calls
console.log(iterator.next()); // "{ value: undefined, done: true }"
(二)何为生成器
1. 生成器的含义
ES5创建迭代器的方式比较复杂,ES6提供了生成器来创建迭代器。生成器:
- 是可以返回一个迭代器的函数,能被用于所有可使用函数的位置;
- 由放在function后面的*来表示, ES6没有规定function与*之间是否要有空格;
- 能使用yield关键字,yield指定了next()方法调用迭代器时应当按顺序返回的值,生成器会在每个yield语句后停止执行。
ES6用生成器来创建迭代器
// generator能像普通函数一样被调用,但会返回一个迭代器
function *createIterator() {
yield 1;
yield 2;
yield 3;
}
// generator能像普通函数一样被调用,但会返回一个迭代器
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
yield关键字只能用于生成器内部,用于其他任意位置都会报语法错误,即使在生成器内部嵌套的函数中也不行。
function *createIterator(items) {
items.forEach(function(item) {
// syntax error
yield item + 1;
});
}
2. 生成器写法
1)函数声明 function* name() {}
// 函数声明
function* createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
};
let iterator = createIterator([1, 2, 3]);
2)函数表达式 function* () {}
// 函数表达式
let createIterator = function* (items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
};
let iterator = createIterator([1, 2, 3]);
不能用箭头函数创建生成器
3)对象方法
createIterator: function *(){}
*createIterator() {}
var o = {
// ES5 对象字面量的写法
createIterator: function* (items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = o.createIterator([1, 2, 3]);
var o = {
// ES6 速记法
*createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = o.createIterator([1, 2, 3]);
这三种写法生成器的功能都没有区别,只是有语法区别。
(三)可迭代对象
1. 可迭代对象
可迭代对象(iterable)是包含Symbol.iterator属性的对象,与迭代器紧密相关。
Symbol.iterator定义了为指定对象返回迭代器的函数。
ES6中所有的集合对象(数组、Set、Map)和字符串都是可迭代对象,有默认的迭代器,注意自定义创建的对象不是可迭代的。
生成器默认会为Symbol.iterator属性赋值,所以生成器创建的所有迭代器都是可迭代对象。
2. 可迭代对象与for-of循环
for-of循环在循环每次执行时都会调用可迭代对象的next()方法,并将结果对象的value值存储在一个变量上,循环过程会持续到结果对象的done属性变成true为止。
let values = [1, 2, 3];
for (let num of values) {
console.log(num);
}
for-of循环的执行过程:
- 调用values数组的
Symbol.iterator方法,创建一个迭代器;(此调用由js引擎在后台执行) - 调用迭代器的next()方法,读取结果对象的value属性并放入到num变量;
- 当结果对象的done变成true,循环立即结束,不会再去读取value属性,所以num变量的值不会为undefined。
若仅需要迭代数组和集合的值,推荐使用for-of循环而非for循环。
在不可迭代对象、null或undefined上使用for-of语句,会抛出错误。
3. Symbol.iterator
1)使用Symbol.iterator访问对象上的默认迭代器
// 此代码获取了values数组的默认迭代值,并用它来迭代数组中的项。这与使用for-of循环时在后台发生的过程一致。
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 }"
2)使用Symbol.iterator检测一个对象是否能被迭代
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
3)使用Symbol.iterator自定义可迭代对象 自定义对象默认是不可迭代的,可以创建一个包含生成器的Symbol.iterator属性,将他们变成可迭代对象。
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);
}
4. 可迭代对象与扩展运算符
扩展运算符(…)可作用于所有可迭代对象,且会使用默认迭代器来判断需要使用哪些值。
let set = new Set([1, 2, 3, 3, 3, 4, 5]),
array = [...set];
console.log(array); // [1,2,3,4,5]
let map = new Map([ ["name", "Nicholas"], ["age", 25]]),
array = [...map];
console.log(array); // [ ["name", "Nicholas"], ["age", 25]]
let smallNumbers = [1, 2, 3],
bigNumbers = [100, 101, 102],
allNumbers = [0, ...smallNumbers, ...bigNumbers];
console.log(allNumbers.length); // 7
console.log(allNumbers); // [0, 1, 2, 3, 100, 101, 102]
(四)内置的迭代器
1. 集合的迭代器
ES6有三种集合对象类型: 数组、Map和Set,他们都拥有以下三种迭代器:
- entries(): 返回键值对
- values(): 返回值
- keys(): 返回键
集合类型的默认迭代器:
数组和Set类型的默认迭代器是:values();
Map的默认迭代器是:entries()
Map默认迭代器的行为有助于在for-of循环中使用解构:
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");
// same as using data.entries()
for (let [key, value] of data) {
console.log(key + "=" + value);
}
2. 字符串的迭代器
字符串的默认迭代器能处理字符,而不是码元。
var message = "A ð ®· B";
for (let c of message) {
console.log(c);
}
A
ð
®
·
B
3. NodeList的迭代器
文档对象模型(DOM)具有一种NodeList类型,用于表示页面文档中元素的集合。
NodeList默认迭代器的规定在HTML规范而非ES6规范中,其表现方式与数组的默认迭代器一致,因此可以将NodeList用于for-of循环,或其他使用对象默认迭代器的场合。
var divs = document.getElementsByTagName("div");
for (let div of divs) {
console.log(div.id);
}
(五) 迭代器高级功能
1. 传参给迭代器next()
通过next()方法项迭代器内部传参,该参数会成为上一个yield表达式的返回值。
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // 4 + 2
yield second + 3; // 5 + 3
}
let iterator = createIterator();
console.log(iterator.next()); // "{ 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 }"
next()方法的运行逻辑:
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的结果对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
注意:由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数时无效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带参数。
2. 在迭代器中抛出错误throw()
能传递给迭代器的不仅是数据,还可以是错误条件。使用throw()方法,让迭代器在恢复执行时可以抛出一个错误。
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // yield 4 + 2, then throw
yield second + 3; // never is executed
}
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"))); // error thrown from generator
可以在生成器内部使用try-catch捕捉错误:
function *createIterator() {
let first = yield 1;
let second;
try {
second = yield first + 2; // yield 4 + 2, then throw
} catch (ex) {
second = 6; // on error, assign a different value
}
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: 9, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
将next()和throw()都当做迭代器的指令,会有助于思考。next()方法只是迭代器继续执行(可能会带着给定的值),而throw()方法则指示迭代器通过抛出一个错误继续执行。在调用点之后会发生什么,根据生成器内部的代码来决定。
3. 生成器的return语句
生成器是函数,可以使用return语句,让生成器提早退出执行,也可以用return指定在next()方法最后一次调用时的返回值。
function *createIterator() {
yield 1;
return; // 让生成器提早退出执行 后面的yield都是不可达的
yield 2;
yield 3;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
function *createIterator() {
yield 1;
return 42;
}
let iterator = createIterator();
console.log(iterator.next()); // "{ value: 1, done: false }"
console.log(iterator.next()); // "{ value: 42, done: true }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
注意:
- 在return语句中指定的任意值都只会在结果对象中出现一次,此后value字段就会被重置为undefined。
- 扩展运算符(…)和for-of循环会忽略return语句所指定的值,一旦他们看懂啊done的值为true,就会停止操作而不会读取对应的value值。
4. 生成器委托
生成器可以用*配合yield来委托其他的迭代器。
// 生成器委托
function* createNumberIterator() {
yield 1
yield 2
}
function* createColorIterator() {
yield 'red'
yield 'green'
}
function* createCombinedIterator() {
// 生成器依次委托了createNumberIterator()和createColorIterator()
yield* createNumberIterator()
yield* createColorIterator()
yield true
}
var 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 }"
使用生成器的返回值:
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 result;
yield *createRepeatingIterator(result);
}
var iterator = createCombinedIterator();
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: "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 }"
注意,上面例子中的值3从未在对于next()方法的任何调用中被输出,当前它仅仅存在于createCombinedIterator()生成器内部,除非我们把yield result;的注释去掉,用yield语句明确将此返回值输出。
(六)异步任务运行
最常见的异步调用方式:使用回调函数,容易形成回调地狱。
而yield能停止运行,并在重新开始运行前等待next()方法被调用,可以在没有回调函数的情况下实现异步调用。
1. 一个简单的任务运行器
// taskDef是一个生成器函数
function run(taskDef) {
// 创建迭代器,让它在别处可用
let task = taskDef();
// start the task
let result = task.next();
// 递归调用函数来保持对next()的调用
function step() {
// if there's more to do
if (!result.done) {
result = task.next();
step();
}
}
// start the process
step();
}
run(function*() {
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
});
2. 带数据的任务运行器
更改上面例子中step()函数的方法体内容:
function step() {
if (!result.done) {
// 将result.value作为next()的参数
result = task.next(result.value);
step();
}
}
run(function*() {
let value = yield 1;
console.log(value); // 1
value = yield value + 3;
console.log(value); // 4
});
3. 异步任务运行器
传统的执行异步操作的方式是调用一个包含回调的函数,例如:考虑在Node.js中从磁盘读取一个文件:
let fs = require("fs");
fs.readFile("config.json", function(err, contents) {
if (err) {
throw err;
}
doSomethingWith(contents);
console.log("Done");
});
当需要完成的任务很少时,这么做很有效,然而当需要嵌套回调函数,或者要按顺序处理一系列的异步任务时,此方式就会非常麻烦,在这种场合下,生成器和yield会带来帮助:
function run(taskDef) {
// create the iterator, make available elsewhere
let task = taskDef();
// start the task
let result = task.next();
// recursive function to keep calling next()
function step() {
// if there's more to do
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();
}
}
}
// start the process
step();
}
let fs = require("fs");
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback);
};
}
run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});
此例执行了异步的readFile()操作,而在主要代码中并未暴露出任何回调函数。除了yield之外,此代码看起来与同步代码并无不同。