迭代器(Iterator)和生成器(Generator)
迭代器(Iterator)
1.什么是迭代器Iterator
- 迭代器是一种特殊对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个next()方法,每次调用都返回一个结果对象。结果对象有两个属性:一个是value,表示下一个将要返回的值;另一个是done,它是一个布尔类型的值,当没有更多可返回数据时返回true。迭代器还会保存一个内部指针,用来指向当前集合中值的位置,每调用一次next()方法,都会返回下一个可用的值。
- 如果在最后一个值返回后再调用next()方法,那么返回的对象中属性done的值为true,属性value则包含迭代器最终返回的值,这个返回值不是数据集的一部分,它与函数的返回值类似,是函数调用过程中最后一次给调用者传递信息的方法,如果没有相关数据则返回undefined
2.集合对象迭代器
在ES6中,所有的集合对象(数组、Set集合及Map集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。ES6中新加入的特性for-of循环需要用到可迭代对象的这些功能:
- entries() 返回键值对迭代器: Map默认迭代器
- keys() 返回键名迭代器
- values() 返回集合值迭代器: Array、Set默认迭代器
const map = new Map([['name', 'Jony'], ['age', 25]]);
const mapIterator = map.entries();
mapIterator.next(); // " {done: false,value: ['name', 'Jony']}
mapIterator.next(); // {done: false,value: ['age', '25']}
mapIterator.next(); // {done: true,value: undefined}
3.什么是生成器Generator
生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中会用到新的关键字yield。星号可以紧挨着function关键字,也可以在中间添加一个空格
// 生成器
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
- 在这个示例中,
createlterator()
前的星号表明它是一个生成器;yield
关键字也是ES6
的新特性,可以通过它来指定调用迭代器的next()
方法时的返回值及返回顺序。生成迭代器后,连续3次调用它的next()
方法返回3个不同的值,分别是1、2和3。生成器的调用过程与其他函数一样,最终返回的是创建好的迭代器 - 生成器函数最有趣的部分是,每当执行完一条
yield
语句后函数就会自动停止执行。举个例子,在上面这段代码中,执行完语句yield 1
之后,函数便不再执行其他任何语句,直到再次调用迭代器的next()
方法才会继续执行yield2
语句。生成器函数的这种中止函数执行的能力有很多有趣的应用 - 使用yield关键字可以返回任何值或表达式,所以可以通过生成器函数批量地给迭代器添加元素。例如,可以在循环中使用
yield
关键字
function *createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
let 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 }"// 之后的所有调用
console.log(iterator.next()); // "{ value: undefined, done: true }"
- 在此示例中,给生成器函数
createlterator()
传入一个items
数组,而在函数内部,for
循环不断从数组中生成新的元素放入迭代器中,每遇到一个yield
语句循环都会停止;每次调用迭代器的next()
方法,循环会继续运行并执行下一条yield
语句
【使用限制】
yield
关键字只可在生成器内部使用,在其他地方使用会导致程序抛出错误
function *createIterator(items) {
items.forEach(function(item) {
// 语法错误
yield item + 1;
});
}
- 从字面上看,
yield
关键字确实在createlterator()函数内部,但是它与return
关键字一样,二者都不能穿透函数边界。嵌套函数中的return
语句不能用作外部函数的返回语句,而此处嵌套函数中的yield
语句会导致程序抛出语法错误
【生成器对象的方法】
由于生成器本身就是函数,因而可以将它们添加到对象中。例如,在ES5风格的对象字面量中,可以通过函数表达式来创建生成器
var o = {
createIterator: function *(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = o.createIterator([1, 2, 3]);
也可以用
ES6
的函数方法的简写方式来创建生成器,只需在函数名前添加一个星号(*)
var o = {
*createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = o.createIterator([1, 2, 3]);
4.创建可迭代对象
默认情况下,开发者定义的对象都是不可迭代对象,但如果给Symbol.iterator
属性添加一个生成器,则可将其变为可迭代对象
let collection = {
items: [],
*[Symbol.iterator]() {
for (let item of items) {
yield item;
}
}
};
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
console.log(x);
}
5.访问默认的迭代器
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 }" //
6.字符串迭代器
const message = "A 𠮷 B";
const message = "A 中 B";
for (let i = 0; i < message.length; i++) {
console.log(message[i]); // A ()��()B // A () () () () B
}
for (let item of message) {
console.log(item);// A () 𠮷 () B
}
- 由于双字节字符被视作两个独立的编码单元,从而最终在A与B之间打印出4个空行
- 所幸,ES6的目标是全面支持Unicode,并且我们可以通过改变字符串的默认迭代器来解决这个问题,使其操作字符而不是编码单元。现在,修改前一个示例中字符串的默认迭代器,让for-of循环输出正确的内容
7.NodeList迭代器
const divs = document.getElementsByTagName('div');
for (let div of divs) {
console.log(div.id);
}
8.展开运算符和非数组可迭代对象
通过展开运算符(...
)可以把Set
集合转换成一个数组
let set = new Set([1, 2, 3, 3, 3, 4, 5]),
array = [...set];
console.log(array); // [1,2,3,4,5]
- 这段代码中的展开运算符把
Set
集合的所有值填充到了一个数组字面量里,它可以操作所有可迭代对象,并根据默认迭代器来选取要引用的值,从迭代器读取所有值。然后按照返回顺序将它们依次插入到数组中。Set
集合是一个可迭代对象,展开运算符也可以用于其他可迭代对象
let map = new Map([ ["name", "huochai"], ["age", 25]]),
array = [...map];
console.log(array); // [ ["name", "huochai"], ["age", 25]]
- 展开运算符把
Map
集合转换成包含多个数组的数组,Map
集合的默认迭代器返回的是多组键值对,所以结果数组与执行new Map()
时传入的数组看起来一样 - 在数组字面量中可以多次使用展开运算符,将可迭代对象中的多个元素依次插入新数组中,替换原先展开运算符所在的位置
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]
- 创建一个变量
allNumbers
,用展开运算符将smallNumbers
和bigNumbers
里的值依次添加到allNumbers
中。首先存入0,然后存入small
中的值,最后存入bigNumbers
中的值。当然,原始数组中的值只是被复制到allNumbers
中,它们本身并未改变 - 由于展开运算符可以作用于任意可迭代对象,因此如果想将可迭代对象转换为数组,这是最简单的方法。既可以将字符串中的每一个字符(不是编码单元)存入新数组中,也可以将浏览器中
NodeList
对象中的每一个节点存入新的数组中
9.例子
例子A
let state = function*(){
while(1){
yield 'A';
yield 'B';
yield 'C';
}
}
let status = state();
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'
console.log(status.next().value);//'C'
console.log(status.next().value);//'A'
console.log(status.next().value);//'B'
例子B
function *createIterator() {
console.log('begin');
let a = yield 1;
console.log('a', a);
let b = yield a + 2;
console.log('b', b);
yield b + 3;
console.log('finish');
}
let iterator = createIterator();
iterator.next(0); // return {value: 1, done: false}
// begin
iterator.next(2); // return {value: 4, done: false}
// a 2
iterator.next(4);// return {value: 6, done: false}
// b 4
iterator.next(6);// return {value: undefined, done: true}
// finish
- 通过上面高级迭代器的代码和输出,我们可以看出迭代器的运行原理。生成器Generator的代码根据
yield
关键字被拆分成了多个函数,类似于split('yield')
的效果。在运行了iterator.next(0);
之后createIterator执行了console.log('begin'); yield 1;
。至于let a = yield 1;
,相当于return 1
和let a = next()
,所以a的值是iterator.next(2);
的参数2。
例子C
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()
方法时无论传入什么参数都会被丢弃。由于传给next()
方法的参数会替代上一次yield
的返回值,而在第一次调用next()
方法前不会执行任何yield
语句,因此在第一次调用next()
方法时传递参数是毫无意义的 - 第二次调用
next()
方法传入数值4作为参数,它最后被赋值给生成器函数内部的变量first
。在一个含参yield语句中,表达式右侧等价于第一次调用next()
方法后的下一个返回值,表达式左侧等价于第二次调用next()
方法后,在函数继续执行前得到的返回值。第二次调用next()
方法传入的值为4,它会被赋值给变量first
,函数则继续执行。第二条yield
语句在第一次yield
的结果上加了2,最终的返回值为6 - 第三次调用
next()
方法时,传入数值5,这个值被赋值给second
,最后用于第三条yield
语句并最终返回数值8
10.在迭代器中抛出错误
除了给迭代器传递数据外,还可以给它传递错误条件。通过
throw()
方法,当迭代器恢复执行时可令其抛出一个错误。这种主动抛出错误的能力对于异步编程而言至关重要,也能提供模拟结束函数执行的两种方法(返回值或抛出错误),从而增强生成器内部的编程弹性。将错误对象传给throw()
方法后,在迭代器继续执行时其会被抛出
function *createIterator() {
let first = yield 1;
let second = yield first + 2; // yield 4 + 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"))); // 从生成器中抛出了错误
console.log(iterator.next(5)); // "{ value: undefined, done: true }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
捕获错误
function *createIterator() {
let first = yield 1;
let second;
try {
second = yield first + 2; // yield 4 + 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: 9, done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"
- 在此示例中,
try-catch
代码块包裹着第二条yield
语句。尽管这条语句本身没有错误,但在给变量second
赋值前还是会主动抛出错误,catch
代码块捕获错误后将second
变量赋值为6,下一条yield
语句继续执行后返回9 - 这里有一个有趣的现象调用
throw()
方法后也会像调用next()
方法一样返回一个结果对象。由于在生成器内部捕获了这个错误,因而会继续执行下一条yield
语句,最终返回数值9 - 如此一来,
next()
和throw()
就像是迭代器的两条指令,调用next()
方法命令迭代器继续执行(可能提供一个值),调用throw()
方法也会命令迭代器继续执行,但同时也抛出一个错误,在此之后的执行过程取决于生成器内部的代码 - 在迭代器内部,如果使用了
yield
语句,则可以通过next()
方法和throw()
方法控制执行过程,当然,也可以使用return
语句返回一些与普通函数返回语句不太一样的内容
11.生成器返回语句
function *createIterator() {
yield 1;
return;
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 }"
12.委托生成器
function *createNumberIterator() {
yield 1;
yield 2;
}
function *createColorIterator() {
yield "red";
yield "green";
}
function *createCombinedIterator() {
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 *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: "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 }"
- 在生成器
createCombinedlterator()
中,执行过程先被委托给了生成器createNumberlterator()
,返回值会被赋值给变量result
,执行到return 3
时会返回数值3。这个值随后被传入createRepeatinglterator()
作为它的参数,因而生成字符串"repeat"的yield
语句会被执行三次 - 无论通过何种方式调用迭代器
next()
方法,数值3都不会被返回,它只存在于生成器createCombinedlterator()
的内部。但如果想输出这个值,则可以额外添加一条yield
语句
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 }"
[注意]
yield*
也可直接应用于字符串,例如yield* "hello"
,此时将使用字符串的默认迭代器
function *createStringIterator() {
yield* "hello"
}
var iterator = createStringIterator();
console.log(iterator.next()); // "{ value: 'h', done: false }"
console.log(iterator.next()); // "{ value: 'e', done: false }"
console.log(iterator.next()); // "{ value: 'l', done: false }"
console.log(iterator.next()); // "{ value: 'l', done: false }"
console.log(iterator.next()); // "{ value: 'o', done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }" //
13.异步任务执行
生成器令人兴奋的特性多与异步编程有关,
JS
中的异步编程有利有弊:简单任务的异步化非常容易;而复杂任务的异步化会带来很多管理代码的挑战。由于生成器支持在函数中暂停代码执行,因而可以深入挖掘异步处理的更多用法
执行异步操作的传统方式一般是调用一个函数并执行相应回调函数
// 从磁盘读取文件
let fs = require("fs");
fs.readFile("config.json", function(err, contents) {
if (err) {
throw err;
}
doSomethingWith(contents);
console.log("Done");
});
- 调用
fs.readFile()
方法时要求传入要读取的文件名和一个回调函数,操作结束后会调用该回调函数并检查是否存在错误,如果没有就可以处理返回的内容。如果要执行的任务很少,那么这样的方式可以很好地完成任务;如若需要嵌套回调或序列化一系列的异步操作,事情会变得非常复杂。此时,生成器和yield
语句就派上用场了
【简单任务执行器】
由于执行
yield
语句会暂停当前函数的执行过程并等待下一次调用next()
方法,因此可以创建一个函数,在函数中调用生成器生成相应的迭代器,从而在不用回调函数的基础上实现异步调用next()
方法
function run(taskDef) {
// 创建迭代器,让它在别处可用
let task = taskDef();// 启动任务
let result = task.next();// 递归使用函数来保持对 next() 的调用
function step() {
// 如果还有更多要做的
if (!result.done) {
result = task.next();
step();
}
}
// 开始处理过程
step();
}
- 函数
run()
接受一个生成器函数作为参数,这个函数定义了后续要执行的任务,生成一个迭代器并将它储存在变量task
中。首次调用迭代器的next()
方法时,返回的结果被储存起来稍后继续使用。step()
函数会检查result.done
的值,如果为false
则执行迭代器的next()
方法,并再次执行step()
操作。每次调用next()
方法时,返回的最新信息总会覆写变量result
。在代码的最后,初始化执行step()
函数并开始整个的迭代过程,每次通过检查result.done
来确定是否有更多任务需要执行 - 借助这个
run()
函数,可以像这样执行一个包含多条yield
语句的生成器
run(function*() {
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
});
- 这个示例最终会向控制台输出多次调用
next()
方法的结果,分别为数值1、2和3。当然,简单输出迭代次数不足以展示迭代器高级功能的实用之处,下一步将在迭代器与调用者之间互相传值
【向任务执行器传递数据】
给任务执行器传递数据的最简单办法是,将值通过迭代器的
next()
方法传入作为yield
的生成值供下次调用。在这段代码中,只需将result.value
传入next()
方法即可
function run(taskDef) { // 创建迭代器,让它在别处可用
let task = taskDef(); // 启动任务
let result = task.next(); // 递归使用函数来保持对 next() 的调用
function step() {
// 如果还有更多要做的
if (!result.done) {
result = task.next(result.value);
step();
}
}
// 开始处理过程
step();
}
- 现在
result.value
作为next()
方法的参数被传入,这样就可以在yield
调用之间传递数据了
run(function*() {
let value = yield 1;
console.log(value); // 1
value = yield value + 3;
console.log(value); // 4
});
- 此示例会向控制台输出两个数值1和4。其中,数值1取自
yield 1
语句中回传给变量value
的值;而4取自给变量value
加3后回传给value
的值。现在数据已经能够在yield
调用间互相传递了,只需一个小小改变便能支持异步调用
【异步任务执行器】
之前的示例只是在多个
yield
调用间来回传递静态数据,而等待一个异步过程有些不同。任务执行器需要知晓回调函数是什么以及如何使用它。由于yield
表达式会将值返回给任务执行器,所有的函数调用都会返回一个值,因而在某种程度上这也是一个异步操作,任务执行器会一直等待直到操作完成
- 下面定义一个异步操作
function fetchData() {
return function(callback) {
callback(null, "Hi!");
};
}
- 本示例的原意是让任务执行器调用的所有函数都返回一个可以执行回调过程的函数,此处
fetchData()
函数的返回值是一个可接受回调函数作为参数的函数,当调用它时会传入一个字符串"Hi!"
作为回调函数的参数并执行。参数callback
需要通过任务执行器指定,以确保回调函数执行时可以与底层迭代器正确交互。尽管fetchData()
是同步函数,但简单添加一个延迟方法即可将其变为异步函数
function fetchData() {
return function(callback) {
setTimeout(function() {
callback(null, "Hi!");
}, 50);
};
}
- 在这个版本的
fetchData()
函数中,让回调函数延迟了50ms再被调用,所以这种模式在同步和异步状态下都运行良好。只需保证每个要通过yield关键字调用的函数都按照与之相同的模式编写 - 理解了函数中异步过程的运作方式,可以将任务执行器稍作修改。当
result.value
是一个函数时,任务执行器会先执行这个函数再将结果传入next()
方法
function run(taskDef) {
// 创建迭代器,让它在别处可用
let task = taskDef();// 启动任务
let result = task.next();// 递归使用函数来保持对 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
是一个函数,会传入一个回调函数作为参数调用它,回调函数遵循Node.js
有关执行错误的约定:所有可能的错误放在第一个参数(err
)中,结果放在第二个参数中。如果传入了err
,意味着执行过程中产生了错误,这时通过task.throw()
正确输出错误对象;如果没有错误产生,data
被传入task.next()
作为结果储存起来,并继续执行step()
。如果result.value
不是一个函数,则直接将其传入next()
方法 - 现在,这个新版的任务执行器已经可以用于所有的异步任务了。在
Node.js
环境中,如果要从文件中读取一些数据,需要在fs.readFile()
外围创建一个包装器(wrapper
),并返回一个与fetchData()
类似的函数
let fs = require("fs");
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback);
};
}
- readFile()接受一个文件名作为参数,返回一个可以执行回调函数的函数。回调函数被直接传入fs.readFile()方法,读取完成后会执行它
run(function*() {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
});
- 在这段代码中没有任何回调变量,异步的
readFile()
操作却正常执行,除了yield
关键字外,其他代码与同步代码完全一样,只不过函数执行的是异步操作。所以遵循相同的接口,可以编写一些读起来像是同步代码的异步逻辑 - 当然,这些示例中使用的模式也有缺点,也就是不能百分百确认函数中返回的其他函数一定是异步的。着眼当下,最重要的是能理解任务执行过程背后的理论知识