一、基本概念
iterator
(迭代器)提供一种统一的接口,用以遍历一个集合型的数据结构。只要一个数据结构实现了iterator接口,那么它就能够:
1)使用for-of
遍历
2)被展开运算符(...
)展开
使用Typescript来描述迭代器接口的话,可以表示如下:
interface Iterable {
[Symbol.iterator](): Iterator
}
interface Iterator {
next(value?: any): IterationResult
}
interface IterationResult {
value: any,
done: boolean
}
这说明:
1)如果一个对象要实现iterator接口,那么它需要拥有[Symbol.iterator]
方法
2)[Symbol.iterator]()
方法的返回的是一个迭代器对象,迭代器对象中应该有next()
方法
3)next()
方法的返回值是一个对象,形式如:{ value: any, done: boolean }
二、基本用法
当我们实现了iterator接口后,我们就可以使用for-of
来访问数据结构了,如:
const ds = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
ds[Symbol.iterator] = function() {
let i = 0;
const that = this;
return {
next() {
return { value: that[i], done: ++i > that.length };
}
}
}
for (let x of ds) {
console.log(x);
}
/*
输出:
a
b
c
*/
如果在对象自身上没有部署[Symbol.iterator]()
方法,那么其原型链中存在的这个方法,也是可以的,如:
function F(){}
F.prototype = {
constructor: F,
[Symbol.iterator]() {
let i = 0;
return {
next() {
let iterator = { value: undefined, done: true };
if (i < 3) {
iterator.value = i++;
iterator.done = false;
}
return iterator;
}
};
}
}
const f = new F();
for (let x of f) {
console.log(x);
}
/*
输出:
0
1
2
*/
对于Array-Like的结构(即拥有length
属性的对象),可以用如下的方法便捷地使用数组的iterator接口,如:
const obj = { 0: 'a', 1: 'b', length: 2 };
[...obj]; // Uncaught TypeError: obj is not iterable
// 此时,我们可以这么做:
obj[Symbol.iterator] = Array.prototype[Symbol.iterator];
[...obj]; // ['a', 'b']
当然,也可以写作:
obj[Symbol.iterator] = [][Symbol.iterator];
不过,前者要快一些。
如果一个[Symbol.iterator]()
的返回值不是一个遍历器对象,那么会报错:
const obj = {
[Symbol.iterator]() {
return 1;
}
}
[...obj]; // TypeError: Result of the Symbol.iterator method is not an object
注意:遍历器对象中,可以缺省done: false
或者value: undefined
,因为这是默认值
三、Iterator接口与Generator函数
iterator接口可以和generator函数一起使用,如:
const obj = {
* [Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
yield* [4, 5, 6];
}
}
[...obj]; // 得到 [1, 2, 3, 4, 5, 6]
四、遍历器对象的return()
、throw()
return()
方法的作用是:当使用for-of
时,如果循环提前退出(使用break
或者抛出错误),那么就会被调用,如:
const obj = {
[Symbol.iterator]() {
return {
next() {
console.log('next() was called');
return { done: false };
},
return() {
console.log('return() was called');
return { done: true };
}
}
}
}
所以有:
for (let x of obj) {
break;
}
/*
输出:
next() was called
return() was called
*/
for (let x of obj) {
throw new Error();
}
/*
输出:
next() was called
return() was called
*/
五、for-of循环
在ES6之前,遍历一个数组,最早采用的方式是:
for (var i=0; i<=arr.length; i++) { /* 使用arr[i] */ }
也有人用for-in
,如:
for (var i in arr) { /* 使用arr[i] */ }
但是这两种方式都只能获取键名(在数组中称为下标),而不能直接获得值,所以ES5提供了forEach()
,可以有:
arr.forEach(function(x) {
/* 使用x */
});
而ES6里,则更方便,原生提供了for-of
这种语句结构,所以可以:
for (let x of arr) {
/* 使用x */
}
for-of
实际上,调用的是[Symbol.iterator]()
,所以,只要一个对象实现了iterator接口,那么它就能够使用for-of
。
需要注意的是:数组中,for-of
只遍历具有数字索引的属性,而忽略其他非数组索引的属性,而for-in
则不忽略,如:
const arr = [1, 2, 3];
arr.foo = 'foo';
for (let x of arr) {
console.log(x);
}
/* 输出:1 2 3 */
for (let i in arr) {
console.log(arr[i]);
}
/* 输出:1 2 3 foo */
在ES6里,由于Array
、Set
、Map
、String
、TypedArray
、arguments对象
、NodeList对象
都原生实现了iterator接口,所以它们都可以使用for-of
遍历,而对象原生是没有实现for-of
的,所以不能遍历,如:
const obj = { a: 1, b: 2, c: 3 };
for (let x of obj) {
console.log(x);
}
// TypeError: obj is not iterable
为此,如果我们想遍历对象的话,那么我们可以这么实现:
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for (let [key, val] of entries(obj)) {
console.log(key, val);
}
/*
输出:
a 1
b 2
c 3
*/