18、Iterator和for…of循环
1.Iterator(遍历器)的概念
遍历器是一种接口,任何数据结构部署了Iterator接口,就可以完成遍历操作
Iterator的遍历过程如下
- 1、创建一个指针对象,指向当前数据结构的起始位置
- 2、第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员
- 3、第二次调用指针对象的next方法,指针指向数据结构的第二个成员
- 4、以此类推,不断调用指针的对象的next方法,直到它指向数据结构结束的位置
第一次调用next方法,会返回数据结构的当前成员的信息,也就是返回一个包含value和done两个属性的对象,value属性是当前成员的值,done属性表示遍历是否结束。
下面是模拟next方法返回值的例子
function makeIterator(arr) {
let nextIndex = 0;
return {
next: function() {
return nextIndex < arr.length ? {value: arr[nextIndex++], done: false} :
{value: undefined, done: true};
}
}
};
let it = makeIterator(['a', 'b']);
it.next(). // {value: 'a', done: false}
it.next() // {value: 'b', done: false}
it.next() // {value: undefined, done: true}
对于遍历器对象,done: false和value:undefined属性都是可以忽略的,因此上面的makeIterator函数可以简写为以下形式:
function makeIterator(arr) {
let nextIndex = 0;
return {
next: function() {
return nextIndex < arr.length ? {value: arr[nextIndex++]} : {done: true};
}
}
};
let it = makeIterator(['a', 'b']);
it.next(). // {value: 'a'}
it.next() // {value: 'b'}
it.next() // {done: true}
2.默认Iterator接口
默认的Iterator接口部署在数据结构的Symbol.iterator属性上,当一个数据结构具有Symbol.iterator属性时,就可以认为它是可遍历的,这个属性本身是一个函数,执行这个函数会返回一个遍历器。
下面是定义了一个可遍历的对象obj
const obj = {
[Symbol.iterator] : function() {
return {
next: function() {
return {
value: 1,
done: true
}
}
}
}
}
在ES6中,有些数据结构原生具备了Iterator接口,即不用任何处理,就可以被for…of循环遍历
原生具备Iterator接口的数据结构如下:
- Array
- Map
- Set
- String
- TypedArray
- arguments对象
- NodeList对象
下面的代码是数组的Symbol.iterator属性
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.ierator]();
iter.next(); //oouput: { value: 'a', done: false }
iter.next(); //oouput: { value: 'b', done: false }
iter.next(); //oouput: { value: 'c', done: false }
iter.next(); //oouput: { value: undefined, done: true }
其他的一些没有部署Iterator接口的数据结构,需要自己在Symbol.iterator属性上面部署,这样才会被for…of循环遍历。
对象(Object)之所以没有部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定,如下所示
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
let value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
}
return {done: true, value: undefined};
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (let value of range(0, 3)) {
console.log(value); // 0 1 2
}
下面的例子是另一个为对象添加Iterator接口的例子
let obj = {
data: ['hello', 'world'],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if(index < self.data.length) {
return {
value: self.data[index++],
done: false
};
}
return { value: undefined, done: true }
}
}
}
}
for (let i of obj) {
console.log(i); // hello world
}
对于类似数组的对象(存在数值键名和length属性),部署Iterator接口,有一个简便方法就是直接引用数组的Iterator接口
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
下面是类数组对象部署接口的具体实例,注意,普通对象部署数组的iterator方法无效。
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item);
}
3.调用Iterator接口的场合
(1)解构赋值
对数组或Set结构进行解构赋值时,会默认调用Symbol.iterator方法
let set = new Set().add('a').add('b').add('c');
let [x, y] = set;
let [first, ...rest] = set;
(2)扩展运算符
扩展运算符会调用默认的Iterator接口
let str = 'hello';
[...str]; //output: ['h', 'e', 'l', 'l', 'o']
(3)yield*
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口
let generator = function*() {
yield 1;
yield* [2, 3, 4];
yield 5;
}
const iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
(4)其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口
- for…of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()
- Promise.all()
- Promise.race()
4.字符串的Iterator接口
字符串是一个类似数组的对象,也原生具有iterator接口
let something = 'hi';
typeof something[Symbol.iterator]
// "function"
let iterator = something[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() //
也可以覆盖原生的Symbol.iterator方法,达到修改遍历器的目的
let str = new String('Hi');
[...str];
str[Symbol.iterator] = function() {
return {
next: function() {
if (this._first) {
this._first = false;
return { value: 'bye', done: false };
} else {
return { done: true };
}
},
_first: true
}
}
[...str] // ["bye"]
str // "hi"
5.Iterator接口与Generator函数
Symbol.iterator方法的最简单实现,就是Generator函数
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
[...myIterable] //[1, 2, 3]
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x)
}
6.遍历器对象的return,throw
遍历器对象除了必须要有next方法,还可以具有return方法和throw方法。
return方法使用的场合是,如果for。。。of循环提前退出(通常是因为出错或者有break语句),就会调用return方法。
function readLinesSync(file) {
return {
[Symbol.iterator]() {
return {
next() {
return { done: false };
},
return() {
file.close();
return { done: true };
}
};
},
};
}
// 情况一
for (let line of readLinesSync(fileName)) {
console.log(line);
break;
}
// 情况二
for (let line of readLinesSync(fileName)) {
console.log(line);
throw new Error();
}
上面的两种情况都会触发return方法,情况一是输出文件的第一行后在执行return方法,关闭这个文件;情况二是在执行return方法关闭文件之后,在抛出错误。return方法必须返回一个对象。
7.for…of循环
一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,就可以用for…of循环遍历它的成员。for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
数组
数组原生就有iterator接口,for…of循环本质上是调用这个接口产生的遍历器
const arr = ['a', 'b', 'c'];
for (let v of arr) {
console.log(v); // a b c
}
for…in循环读取键名,for…of循环读取键值,但是for…of循环只返回具有数字属索引的属性
let arr = [3, 5, 7];
arr.foo = 'hello';
for (let i in arr) {
console.log(i); // "0", "1", "2", "foo"
}
for (let item in arr) {
console.log(item); // 3 5 7
}
上面代码中,for…of循环不会返回数组arr的foo属性
Set和Map结构
Set和Map结构也具有iterator接口,可以直接使用for…of循环遍历
let set = new Set([1, 1, 2, 3]);
for (let item of set) {
console.log(item); // 1 2 3
}
let map = new Map().set('a', 1).set('b', 3);
for (let item of map) {
console.log(item); // ['a', 1]. ['b', 3]
}
类似数组的对象
主要是针对DOM NodeList对象,argiments对象
let str = 'hello';
for (let s of str) {
console.log(s);
}
let paras = document.querySelectorAll('p');
for (let p of paras) {
p.classList.add('test');
}
function printArgs() {
for (let x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
针对字符串,for…of循环还有一个特点,就是会正确识别32位UTF-16字符
for (let x of 'a\uD83D\uDC0A') {
console.log(x); // a. 🐊
}
并不是所有类似数组的对象都具有Iterator接口,一个简便的解决方法就是使用Array.from方法将其转为数组
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
for (let x of arrayLike) {
console.log(x); //报错
}
//正确
for (let x of Array.from(arrayLike)) {
console.log(x);
}
对象
对于普通的对象,for…of不能直接使用,必须部署Iterator接口,但是for…in循环依然可以用来遍历键名。
let es6 = {
edition: 6,
committee: 'TC39',
standard: 'ECMA'
}
for (let i in es6) {
console.log(i);
}
for (let item in es6) {
console.log(item); //error
}
解决办法是使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组;另一个方法是使用Generator函数将对象重新包装一下
方法1:
for (let key of Object.keys(obj)) {
console.log(key + ':' + obj[key]);
}
方法2:
const obj = { a:1, b:2, c:3 };
function* entries(obj) { for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for (let [key, value] of entries(obj)) { console.log(key, value); // a 1 b 2 c 3 }