用循环语句迭代数据时,必须要初始化一个变量来记录每一次迭代在数据集合中的位置,而在许多编程语言中,已经开始通过程序化的方式用迭代器对象返回迭代过程中集合的每一个元素。迭代器的使用可以极大地简化数据操作,于是ECMAScript 6也向JavaScript中添加了这个迭代器特性。新的数组方法和新的集合类型(例如Set集合与Map集合)都依赖迭代器的实现,这个新特性对于高效的数据处理而言是不可或缺的,你也会发现在语言的其他特性中也都有迭代器的身影:新的for-of循环、展开运算符(…),甚至连异步编程都可以使用迭代器。本章将讲解迭代器的诸多使用场景,但在这以前,我们一定要了解一下迭代器被添加到JavaScript背后的历史。
什么是迭代器
迭代器是一种特殊对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个next()方法,每次调用都返回一个结果对象。结果对象有两个属性:一个是value,表示下一个将要返回的值;另一个是done,它是一个布尔类型的值,当没有更多可返回数据时返回true。迭代器还会保存一个内部指针,用来指向当前集合中值的位置,每调用一次next()方法,都会返回下一个可用的值。如果在最后一个值返回后再调用next()方法,那么返回的对象中属性done的值为true,属性value则包含迭代器最终返回的值,这个返回值不是数据集的一部分,它与函数的返回值类似,是函数调用过程中最后一次给调用者传递信息的方法,如果没有相关数据则返回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()); //{done: false, value: 1}console.log(iterator.next()); //{done: false, value: 2}console.log(iterator.next()); //{done: false, value: 3}console.log(iterator.next()); //{done: true, value: undefined}//之后所有的调用都会返回相同的内容console.log(iterator.next()); //{done: true, value: undefined}
什么是生成器
生成器是一种返回迭代器的函数(迭代器是一种特殊的对象),通过function关键字后的星号(*)来表示,函数中会用到新的关键字yield。星号可以紧挨着function关键字,也可以在中间添加一个空格,就像这样:
function* createIterator(params) {
yield 1;
yield 2;
yield 3;
}
// 生成器的调用方式与普通函数相同,只不过返回的是一个迭代器
let iterator = createIterator();
console.log(iterator.next().value); //1 iterator.next()=>{value: 1, done: false}
console.log(iterator.next().value); //2 iterator.next()=>{value: 2, done: false}
console.log(iterator.next().value); //2 iterator.next()=>{value: 3, done: false}
使用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());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
yield关键字只可在生成器内部使用,在其他地方使用会导致程序抛出语法错误
例如
function* createIterator() {
items.forEach(function (item) {
// 语法错误
yield item + 1;
});
}
生成器函数表达式
通过函数表达式创建生成器
let createIterator = function* (items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
};
let iterator = createIterator([1, 2, 3]);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// 之后所有的调用都会返回相同的内容
console.log(iterator.next());
生成器对象的方法
let o = {
createIterator: function* (items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = o.createIterator([1, 2, 3]);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
es6简写方式创建对象方法
函数名前添加一个星号*
let o = {
*createIterator(items) {
for (let i = 0; i < items.length; i++) {
yield items[i];
}
}
};
let iterator = o.createIterator([1, 2, 3]);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
可迭代对象和for-of循环
可迭代对象具有Symbol.iterator属性,是一种与迭代器密切相关的对象。Symbol.iterator通过指定的函数可以返回一个作用于附属对象的迭代器。在ECMAScript 6中,所有的集合对象(数组、Set集合及Map集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。
由于生成器默认会为Symbol.iterator属性赋值,因此所有通过生成器创建的迭代器都是可迭代对象
**
for-of循坏
let values = [1, 2, 3];
for (let num of values) {
console.log(num);
}
for (let num in values) {
console.log(num);
}
这段for-of循环的代码通过调用values数组的Symbol.iterator方法来获取迭代器,这一过程是在JavaScript引擎背后完成的。随后迭代器的next()方法被多次调用,从其返回对象的value属性读取值并存储在变量num中,依次为1、2和3,当结果对象的done属性值为true时循环退出,所以num不会被赋值为undefined
访问默认迭代器
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}
在这段代码中,通过Symbol.iterator获取了数组values的默认迭代器,并用它遍历数组中的元素。在JavaScript引擎中执行for-of循环语句时也会有类似的处理过程。
检测对象是否为可迭代对象
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
创建可迭代对象
默认情况下,开发者定义的对象都是不可迭代对象,但如果给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);
}
在这个示例中,先创建一个生成器(注意,星号仍然在属性名前)并将其赋值给对象的Symbol.iterator属性来创建默认的迭代器;而在生成器中,通过for-of循环迭代this.items并用yield返回每一个值。collection对象默认迭代器的返回值由迭代器this.items自动生成,而非手动遍历来定义返回值。
内建迭代器
集合对象迭代器
entries() 迭代器
返回一个迭代器,其值为多个键值对
let colors = ["red", "green", "blue"];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "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()迭代器
返回一个迭代器,其值为集合的值
let colors = ["red", "green", "blue"];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "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()迭代器会返回集合中存在的每一个键
let colors = ["red", "green", "blue"];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "es6");
data.set("format", "ebook");
for (let entry of colors.keys()) {
console.log(entry);
}
for (let entry of tracking.keys()) {
console.log(entry);
}
for (let entry of data.keys()) {
console.log(entry);
}

不同集合类型的默认迭代器
每个集合类型都有一个默认的迭代器,在for-of循环中,如果没有显式指定则使用默认的迭代器。数组和Set集合的默认迭代器是values()方法,Map集合的默认迭代器是entries()方法。有了这些默认的迭代器,可以更轻松地在for-of循环中使用集合对象。请看以下示例:
代码未指定迭代器则使用默认的迭代器
let colors = ["red", "green", "blue"];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();
data.set("title", "es6");
data.set("format", "ebook");
for (let entry of colors) {
console.log(entry);
}
for (let entry of tracking) {
console.log(entry);
}
for (let entry of data) {
console.log(entry);
}

解构与for-of循坏
let data = new Map();
data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");
for (let [key, value] of data) {
console.log(key + "=" + value);
}
字符串迭代器
var message = "A 或 B";
for (let i = 0; i < message.length; i++) {
console.log(message[i]);
}

NodeList迭代器
<body>
<div id="1"></div>
<div id="2"></div>
<div id="3"></div>
<div id="4"></div>
<div id="5"></div>
<div id="6"></div>
<div id="7"></div>
<div id="8"></div>
</body>
<script>
var divs = document.getElementsByTagName("div");
console.log(divs);
for (let div of divs) {
console.log(div.id);
}
</script>

展开运算符与非数组可迭代对象
set集合转数组
// 先去重
let set = new Set([1, 2, 3, 3, 3, 4, 5]);
// 在转换成数组
array = [...set];
console.log(array); //[1,2,3,4,5];
map集合转数组
let map = new Map([["name", "abc"], ["age", 25]]);
array = [...map];
console.log(array);

在数组字面量中可以多次使用展开运算符(**在函数形参中只能使用一次不定参数… 并且只能放在参数的最后一位**)
**
let small = [1, 2, 3],
big = [100, 101, 102],
all = [0, ...small, ...big];
console.log(all.length); //7
console.log(all); //[0, 1, 2, 3, 100, 101, 102]
高级迭代器功能
给迭代器传递参数
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.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。
如果想理解程序内部的具体细节,想清楚这些会对你很有帮助:在生成器内部,代码每次继续执行前,正在执行的代码是哪一段。图 8-1 通过灰色阴影展示了每次yield前正在执行的代码。
在迭代器中抛出错误
通过throw()方法,当迭代器恢复执行时可令其抛出一个错误
throw()方法
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
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: 9, done: false}
console.log(iterator.next()); //{value: undefined, done: true}
生成器返回语句
生成器也是函数,因此可以通过return语句提前退出函数执行
function* createIterator() {
yield 1;
return;
yield 2;
yield 3;
}
let iterator = createIterator();
console.log(iterator.next());
console.log(iterator.next());

function* createIterator() {
yield 1;
return 42;
}
let iterator = createIterator();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

注 展开运算符合for-of循环语句会直接忽略通过return语句
**
委托生成器
在某些情况下,我们需要将两个迭代器合二为一,这时可以创建一个生成器,再给yield语句添加一个星号,就可以将生成数据的过程委托给其他生成器。
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());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

function* createNumberIterator() {
yield 4;
yield 6;
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());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

注:无论通过何种方式调用迭代器的next()方法,数值3永远不会被返回,它只存在于生成器createCombinedIterator的内部,若想输出这个值,则可以额外添加一条yield语句
function* createNumberIterator() {
yield 4;
yield 6;
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());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
异步任务执行
简单任务执行器
由于执行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(function* () {
console.log(1); //1
yield;
console.log(2); //2
yield;
console.log(3); //3
})
向任务执行器传递数据
result.value作为next()方法的参数被传入,这样就可以在yield调用之间传递数据了,就像这样:
function run(taskDef) {
// 创建一个无使用限制的迭代器
let task = taskDef();
// 开始执行任务
let result = task.next();
// 循坏调用next()函数
function step() {
// 如果任务未完成,则继续执行
if (!result.done) {
result = task.next(result.value);
step();
}
}
// 开始迭代执行
step();
}
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的值
将任务执行器稍作修改。当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();
}
run(function* () {
let contents = yield readFile("config.json");
doSomethingWith(contents);
console.log("Done");
})
小结
迭代器是ECMAScript 6的一个重要组成部分,它是语言中某些关键语言元素的依赖。尽管迭代器看起来好像只是一种通过几个简单API返回一系列值的新特性,但在ECMAScript 6中,它还能被应用于许多更复杂的场景中。
Symbol.iterator被用来定义对象的默认迭代器,内建对象和开发者定义的对象都支持这个特性,通过这个Symbol定义的方法可以返回一个迭代器。如果对象中有Symbol.iterator这个属性,则此对象为可迭代对象。
for-of循环可以持续获取可迭代对象中的值,与传统的for循环迭代相比,for-of循环不需要追踪值在集合中的位置,也不需要控制循环结束的时机,使用起来非常方便,它会自动地从迭代器中读取所有值,如果没有更多可返回的值就自动退出循环。
为了降低for-of的使用成本,ECMAScript 6中的许多值都有默认迭代器。所有的集合类型(例如数组、Map集合与Set集合)都有默认迭代器,字符串同样也有默认迭代器,其可以直接迭代字符串中的字符,避免了遍历编码单元带来的诸多问题。
展开运算符也可以作用于可迭代对象,通过迭代器从对象中读取相应的值并插入到一个数组中。生成器是一类特殊函数,在定义时需要额外添加一个星号(),被调用时会自动创建一个迭代器,并通过关键字yield来标识每次调用迭代器的next()方法时的返回值。
生成器是一类特殊函数,在定义时需要额外添加一个星号(),被调用时会自动创建一个迭代器,并通过关键字yield来标识每次调用迭代器的next()方法时的返回值。
借助生成器委托这个新特性,便可重用已有生成器来创建新的生成器,从而进一步封装更复杂的迭代器行为。新语法使用yield *来标识生成的值,新迭代器的返回值便可取自已有的多个迭代器。
在生成器和迭代器的所有应用场景中,最有趣且最令人兴奋的可能是用来创建更简洁的异步代码。这种方式无须在所有地方定义回调函数,其代码看起来像是同步代码,但实际上使用了yield生成的特性来等待异步操作最终完成。
