# (四)Iterator(遍历器)和for...of循环
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制 任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)
# 一、迭代器和 for...of 浅谈
# 1.1 传统 for 循环
先来看一段标准的 for 循环的代码:
var arr = [1,2,3];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// 1 2 3
注意,我们拿到了里面的元素,但却多做了很多事:
- 我们声明了 i 标索引
- 确定了边界,一旦多层嵌套;
function unique(array) {
var res = [];
for (var i = 0, arrayLen = array.length; i < arrayLen; i++) {
for (var j = 0, resLen = res.length; j < resLen; j++) {
if (array[i] === res[j]) {
break;
}
}
if (j === resLen) {
// 把首次出现的加入到新数组中
res.push(array[i]);
}
}
return res;
}
为了消除这种复杂度以及减少循环中的错误(比如错误使用其他循环中的变量),ES6 提供了迭代器和 for of 循环共同解决这个问题。
# 1.2 iterator(迭代器)
迭代器的描述:
- 是为各种数据结构,提供一个统一的、简便的访问接口,是用于遍历数据结构元素的指针
- 二是使得数据结构的成员能够按某种次序排列;
- 三是
ES6
创造的一种遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。
迭代的过程如下:
- 通过 Symbol.iterator 创建一个迭代器,指向当前数据结构的起始位置;
- 随后通过 next 方法进行向下迭代指向下一个位置:
- next 方法会返回当前位置的对象,对象包含了
value
和done
两个属性; - value 是当前属性的值;
- done 用于判断是否遍历结束,done 为 true 时则遍历结束;
- next 方法会返回当前位置的对象,对象包含了
迭代的内部逻辑应该是:
var it = makeIterator(["a", "b"]);
it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true }
function makeIterator(array) {
let index = 0;
const iterator = {};
iterator.next = function() {
if (index < array.length) return { value: array[index++], done: false };
return { value: undefined, done: true };
};
return iterator;
}
# 1.3 什么是 for...of?
注意这里我们仅提及了 forof 与迭代器的关系。
for...of 的描述:
- for...of 语句在可迭代对象上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句——MDN
- 一个数据结构只要部署了
Symbol.iterator
属性,就被视为具有iterator
接口,就可以用for...of
循环遍历它的成员。
看到这里你会发现for...of
和迭代器总是在一起, for...of
循环内部调用的是数据结构的Symbol.iterator
方法。
举个例子:
const obj = {
value: 1,
};
for (value of obj) {
console.log(value);
}
// TypeError: iterator is not iterable
我们直接 for of 遍历一个对象,会报错,然而如果我们给该对象添加 Symbol.iterator 属性:
const obj = {
value: 1,
};
obj[Symbol.iterator] = function() {
return createIterator([1, 2, 3]);
};
for (value of obj) {
console.log(value);
}
// 1
// 2
// 3
由此,我们也可以发现 for...of
遍历的其实是对象的 Symbol.iterator
属性。
JavaScript 原有的 for...in 循环,只能获得对象的键名,不能直接获取键值。ES6 提供 for...of 循环,允许遍历获得键值。
var arr = ["a", "b", "c", "d"];
for (let a in arr) {
console.log(a); // 0 1 2 3
}
for (let a of arr) {
console.log(a); // a b c d
}
上面代码表明:
- for...in 循环读取键名
- for...of 循环读取键值
for...of 循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟 for...in 循环也不一样。
# 二、默认的 Iterator 接口
Iterator
接口的目的,就是为所有数据结构,提供了一种统一的访问机制。当使用 for...of 循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
拿数组举例:
const item = [1, 2, 3][Symbol.iterator]();
item.next();
item.next();
item.next();
// {value: 1, done: false}
// {value: 2, done: false}
// {value: 3, done: false}
// {value: undefined, done: true}
对于原生部署Iterator
接口的数据结构,不用自己写遍历器生成函数,for...of
循环会自动遍历它们。除此之外,都需要自己在 Symbol.iterator 属性上面部署。
本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。
对象(Object)之所以没有默认部署 Iterator 接口,也是因为对象没法统一进行线性转换
一个对象如果要具备可被 for...of 循环调用的 Iterator 接口,就必须在 Symbol.iterator 的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
class newiterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
// Iterator接口 返回本身
[Symbol.iterator]() {
return this;
}
next() {
if (this.value < this.stop) {
return { value: this.value++, done: false };
}
return { value: undefined, done: true };
}
}
const iterator = new newiterator(0, 3);
for (let key of iterator) {
console.log(key);
}
// 0 1 2
上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator 属性对应一个函数,执行后返回当前对象的遍历器对象。
对于类似数组的对象(存在数值键名和 length 属性),部署 Iterator 接口,有一个简便方法,就是 Symbol.iterator 方法直接引用数组的 Iterator 接口。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
[...document.querySelectorAll("div")]; // 可以执行了
NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。上面代码中,我们将它的遍历接口改成数组的 Symbol.iterator 属性,可以看到没有任何影响。
注意,普通对象部署数组的 Symbol.iterator 方法,并无效果。
let iterable = {
a: "a",
b: "b",
c: "c",
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator],
};
for (let item of iterable) {
console.log(item); // undefined, undefined, undefined
}
如果 Symbol.iterator 方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。
# 三、模拟实现的 for...of
其实模拟实现 for of
也比较简单,就是利用它与 Symbol.iterator 的关系。
function forOf(obj, cb) {
let iterable, result;
if (typeof obj[Symbol.iterator] !== "function")
throw new TypeError(result + " is not iterable");
if (typeof cb !== "function") throw new TypeError("cb must be callable");
iterable = obj[Symbol.iterator]();
result = iterable.next();
while (!result.done) {
cb(result.value);
result = iterable.next();
}
}
# 四、使用 Iterator 接口的场景
有一些场合会默认调用 Iterator 接口(即 Symbol.iterator 方法),除了 for...of 循环,还有几个别的场合。
# 4.1 解构赋值
对数组和 Set 结构进行解构赋值时,会默认调用 Symbol.iterator 方法。
let set = new Set()
.add("a")
.add("b")
.add("c");
let [x, y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
# 4.2 扩展运算符
扩展运算符(...)也会调用默认的 Iterator 接口。
// 例一
var str = "hello";
[...str]; // ['h','e','l','l','o']
// 例二
let arr = ["b", "c"];
["a", ...arr, "d"];
// ['a', 'b', 'c', 'd']
上面代码的扩展运算符内部就调用 Iterator 接口。
实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。
# 4.3 yield*
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let generator = function*() {
yield 1;
yield* [2, 3, 4];
yield 5;
};
var 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.4 其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如 new Map([['a',1],['b',2]]))
- Promise.all()
- Promise.race()
# 五、Iterator 接口与 Generator 函数
Symbol.iterator()
方法的最简单实现,还是使用 ES6 新提出的 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);
}
// "hello"
// "world"
上面代码中,Symbol.iterator()方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。
# 六、遍历器对象的 return(),throw()
遍历器对象除了具有 next()方法,还可以具有 return()方法和 throw()方法。如果你自己写遍历器对象生成函数,那么 next()方法是必须部署的,return()方法和 throw()方法是否部署是可选的。
return()方法的使用场合是,如果 for...of 循环提前退出(通常是因为出错,或者有 break 语句),就会调用 return()方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署 return()方法。
function readLinesSync(file) {
return {
[Symbol.iterator]() {
return {
next() {
return { done: false };
},
return() {
file.close();
return { done: true };
},
};
},
};
}
上面代码中,函数 readLinesSync 接受一个文件对象作为参数,返回一个遍历器对象,其中除了 next()方法,还部署了 return()方法。下面的两种情况,都会触发执行 return()方法。
// 情况一
for (let line of readLinesSync(fileName)) {
console.log(line);
break;
}
// 情况二
for (let line of readLinesSync(fileName)) {
console.log(line);
throw new Error();
}
上面代码中:
- 情况一输出文件的第一行以后,就会执行 return()方法,关闭这个文件;
- 情况二会在执行 return()方法关闭文件之后,再抛出错误。
# 参考
# 写在最后
JavaScript 系列:
- 《JavaScript 内功进阶系列》(已完结) (opens new window)
- 《JavaScript 专项系列》(持续更新) (opens new window)
- 《ES6 基础系列》(持续更新) (opens new window)
关于我
- 花名:余光(沉迷 JS,虚心学习中)
- WX:j565017805
其他沉淀
这是文章所在 GitHub 仓库的传送门 (opens new window),您点的 star
,就是对我最大的鼓励 ~