学习阮一峰老师ES6系列(Iterator[遍历器]和for...of)

79 阅读6分钟

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接口的数据结构如下:

  1. Array
  2. Map
  3. Set
  4. String
  5. TypedArray
  6. arguments对象
  7. 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 }