一、Iterator(遍历器)的存在
1、迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。
2、Iterator的概念
ES6语法中表示“集合”的数据结构有很多,比如 Array、Map、Set、String、arguments、NodeList等。这样就需要一种统一的接口机制,来处理遍历所有不同的数据结构。
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理
该数据结构的所有成员)。
以上数据类型,都有 Symbol.iterator
属性,属性值是一个函数,执行这个函数就会返回一个迭代器。这个迭代器就有 next
方法可顺序迭代子元素。
Iterator 的作用有三个:
- 一是为各种数据结构,提供一个统一的、简便的访问接口;
- 二是使得数据结构的成员能够按某种次序排列;
- 三是 ES6 创造了一种新的遍历命令
for...of
循环,Iterator 接口主要供 for…of 消费
二、Iterator 的遍历过程
Iterator
的遍历过程是这样的:
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的 next
方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的 next
方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的 next
方法,直到它指向数据结构的结束位置。
每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 value
和done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束。
下面是模拟 next
方法返回值的例子。
(1)使用ES6的class语法模拟
class MakeIterator {
constructor(list) {
this.list = list;
this.index = 0;
}
next() {
if (this.index < this.list.length) {
return {
value: this.list[this.index++],
done: false
}
} else {
return {
value: undefined,
done: true
}
}
}
}
// 测试
let iterator = new MakeIterator([1,2,3]);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
结果如图所示:
(2)使用function来演示
//自己定义的遍历器生成函数
function makeIterator(list) {
let index = 0;
// 返回一个遍历器对象
return {
next: function () {
//三元表达式
return index < list.length ?
{value: list[index++], done: false} : {value: undefined, done: true}
}
}
}
var iterator = makeIterator([1,3,5]);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
结果如图所示:
上面代码定义了一个 makeIterator 函数,它是一个遍历器生成函数,作用就是返回一个遍历器对象(指针对象)。指针对象的 next
方法,用来移动指针。开始时,指针指向数组的开始位置。然后,每次调用 next
方法,指针就会指向数组的下一个成员。总之,调用指针对象的next方法,就可以遍历事先给定的数据结构。
注意:Iterator 只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象。就像下边的例子:
function idMaker() {
let index = 0;
return {
next: function () {
return {value: index++, done: false}
}
}
}
//这是一个可以无限运行的遍历器对象
let it = new idMaker();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
// ......
三、默认 Iterator 接口
一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。
ES6 规定,默认的 Iterator
接口部署在数据结构的 Symbol.iterator
属性,或者说,一个数据结构只要具有 Symbol.iterator
属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator
属性本身是一个函数,这个函数是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。 因为属性名 Symbol.iterator
是一个表达式,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。
可以运行 Array.prototype[Symbol.iterator]
来测试,如图所示:
我们可以自定义:
const obj = {
[Symbol.iterator]: function () {
return {
next: function () {
return {value: 0, done: false}
}
}
}
}
// 测试
let iterator = obj[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
上面代码中,对象 obj
是可遍历的(iterable),因为具有 Symbol.iterator
属性。执行这个属性,会返回一个遍历器对象。
ES6 的有些数据结构原生具备 Iterator 接口(比如数组),不用任何处理。原因在于,这些数据结构原生部署了Symbol.iterator属性,另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
下面的例子是数组的 Symbol.iterator
属性(在原型链 Array.prototype
上),调用这个属性,就得到遍历器对象。
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
但是如果 Symbol.iterator
方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。 遍历器对象的根本特征就是具有 next
方法。 每次调用 next
方法,都会返回一个代表当前成员的信息对象,具有 value
和 done
两个属性。当我们使用 for of 循环遍历的时候,打印的就是 value 属性
//测试 1
var obj = {};
obj[Symbol.iterator] = function () {
return 1
}
// Uncaught TypeError: Result of the Symbol.iterator method is not an object
// 翻译:Symbol.iterator方法的返回值不是一个对象
for (let item of obj) {
console.log(item);
}
// 测试 2
// 下面我们让Symbol.iterator方法的返回值是一个对象
var obj2 = {};
obj2[Symbol.iterator] = function () {
return {name: 'webchang'}
}
// Uncaught TypeError: undefined is not a function
// 个人理解:报这个错是因为,执行for of循环的时候,会去遍历器生成函数Symbol.iterator的返回的对象中寻找next方法来执行,
// 但是我们没有定义next方法,那么该对象的next属性就是undefined,undefined当然不能当做函数来执行
for (let item of obj2) {
console.log(item);
}
// 测试 3
// 我们在 Symbol.iterator 返回的对象中增加 next 方法
var obj3 = {};
obj3[Symbol.iterator] = function () {
return {
next: function () {
return 1
}
}
}
// Uncaught TypeError: Iterator result 1 is not an object
// 翻译:遍历器的返回值 1 不是一个对象,它希望我们在 next 方法中返回一个对象
for (let item of obj3) {
console.log(item);
}
// 测试 4-------
var obj4 = {};
obj4[Symbol.iterator] = function () {
return {
next: function () {
// 如果直接返回这样的对象,在执行for of循环的时候,会一直打印undefined,因为该对象没有value属性
// return {name:'webchang'}
// 如果返回带有value属性的对象,执行for of循环的时候,会一直打印value属性的值,因为我们没有给它结束条件
return {value: 111,done: false}
}
}
}
for (let item of obj4) {
console.log(item);
}
// 测试 5。正常运行------
var obj5 = {};
obj5[Symbol.iterator] = function () {
let index = 0;
return {
next: function () {
if (index < 4) {
return {value: index++, done: false}
}
return {value: undefined,done: true}
}
}
}
for (let item of obj5) {
console.log(item);
}
四、for…of循环
来源:我们可以利用 Symbol.iterator
自己封装一个遍历方法:
function each(data) {
let iterator = data[Symbol.iterator]();
let item = iterator.next();
while (!item.done) {
console.log(item.value);
item = iterator.next();
}
}
但是如果每个人在使用的时候都需要自己封装一遍,就太麻烦了。ES6 借鉴 C++、Java、C# 和 Python 语言,引入了 for...of
循环,作为遍历所有数据结构的统一的方法。
介绍: 一个数据结构只要部署了 Symbol.iterator
属性,就被视为具有 iterator
接口,就可以用 for...of
循环遍历它的成员。也就是说,for...of
循环内部调用的是数据结构的 Symbol.iterator
方法。
for…of 循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象,以及字符串。
对于原生部署 Iterator
接口的数据结构,不用自己写遍历器生成函数,for...of
循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for…of循环遍历。
1、数组
数组原生具备 iterator
接口(即默认部署了 Symbol.iterator
属性),for...of
循环本质上就是调用这个接口产生的遍历器。
let arr = ['red','green','blue'];
for (let item of arr) {
console.log(item); // red green blue
}
let obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);
for (let item of obj) {
console.log(item); // red green blue
}
上面代码中,空对象 obj 部署了数组 arr 的 Symbol.iterator 属性,结果 obj 的 for…of 循环,产生了与 arr 完全一样的结果。
另外,for...of
循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟 for...in
循环不一样。
let arr = [1,2,3];
arr.foo = 'webchang';
for (let i in arr) {
console.log(i); // "0" "1" "2" "foo"
}
for (let i of arr) {
console.log(i); // 1 2 3
}
2、Set 和 Map 结构
Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用 for…of 循环。
// 遍历 Set 结构
let names = new Set(['小明','webchang','小红','webchang']);
for (let item of names) {
console.log(item); // 小明 webchang 小红
}
// 遍历 Map 结构
let students = new Map();
students.set('001','小明');
students.set('002','webchang');
students.set('003','小红');
for (let item of students) {
console.log(item);
}
for (let [num,name] of students) {
console.log(num,name);
}
结果如图所示:
上面代码演示了如何遍历 Set 结构和 Map 结构。注意两点,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
3、计算生成的数据结构
有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。
entries()
返回一个遍历器对象,用来遍历[键名, 键值]
组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries
方法。keys()
返回一个遍历器对象,用来遍历所有的键名。values()
返回一个遍历器对象,用来遍历所有的键值。
这三个方法调用后生成的遍历器对象,所遍历的都是计算生成的数据结构。
let arr = ['a', 'b', 'c'];
for (let pair of arr.entries()) {
console.log(pair);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']
4、类似数组的对象
类似数组的对象包括好几类。下面是 for...of
循环用于字符串、DOM NodeList
对象、arguments
对象的例子。
// 字符串
let str = "hello";
for (let s of str) {
console.log(s); // h e l l o
}
// DOM NodeList对象
let paras = document.querySelectorAll("p");
for (let p of paras) {
p.classList.add("test");
}
// arguments对象
function printArgs() {
for (let x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// 'a'
// 'b'
注意:并不是所有类似数组的对象(存在数值键名和length属性)都具有 Iterator 接口
// 这是一个类似数组的对象,但是它没有 Iterator 接口
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
但是我们可以对这些类似数组的对象部署 Iterator 接口,有一个简便方法,就是 Symbol.iterator
方法直接引用数组的 Iterator
接口,例如:
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。上面代码中,我们将它的遍历接口改成数组的Symbol.iterator属性,没有任何影响。
下面是一个类似数组的对象调用数组的 Symbol.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); // 'a', 'b', 'c'
}
注意,普通对象部署数组的 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
}
5、对于普通的对象,for…of 结构不能直接使用
对于普通的对象,for…of 结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理。
let obj = {
name: 'webchang',
age: 18
}
// Uncaught TypeError: obj is not iterable
for (let item of obj) {
console.log(item);
}
一个对象如果要具备可被 for…of循环调用的 Iterator 接口,就必须在Symbol.iterator
的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
class RangeIterator {
constructor(start, end) {
this.value = start;
this.stop = end;
}
[Symbol.iterator]() {
return this;
}
next() {
let value = this.value;
if (value < this.stop) {
this.value++;
return {value: this.value, done: false};
}
return {value: undefined, done: true}
}
}
let range = new RangeIterator(0,3);
for (let item of range) {
console.log(item);
}
console.log(range);
上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器对象。
下面是另一个为对象添加 Iterator 接口的例子。
let obj = {
data: ['web', 'chang'],
[Symbol.iterator]: function () {
let self = this;
let index = 0;
return {
next: function () {
if (index < self.data.length) {
return {value: self.data[index++], done: false}
}
return {value: undefined, done: true}
}
}
}
}
for (let item of obj) {
console.log(item); // "web" "chang"
}
一个案例:
// 想要通过 for of 循环直接遍历出 price 对象中 a、b、c 三个属性对应数组的最小值
let price = {
a: [1, 2, 3],
b: [6, 5, 4],
c: [8, 7, 9],
[Symbol.iterator]() {
let arr = []
for (let [key, value] of Object.entries(this)) {
arr.push(Math.min(...value));
}
let index = 0;
return {
next() {
if (index < arr.length) {
return {value: arr[index++], done: false}
}
return {value: undefined, done: true}
}
}
}
}
for (let item of price) {
console.log(item); // 1 4 7
}
6、与其他遍历语法的比较
(1)以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是for循环。
for (var index = 0; index < myArray.length; index++) {
console.log(myArray[index]);
}
(2)原始的 for 循环写法比较麻烦,因此数组提供内置的forEach方法。
myArray.forEach(function (value) {
console.log(value);
});
这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。
(3)for...in
循环可以遍历数组的键名。
for (var index in myArray) {
console.log(myArray[index]);
}
for...in
循环有几个缺点。
- 数组的键名是数字,但是
for...in
循环是以字符串作为键名的,“0”、“1”、“2”等等。 for...in
循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。- 某些情况下,
for...in
循环会以任意顺序遍历键名。
总之,for...in
循环主要是为遍历对象而设计的,不适用于遍历数组。
for...of
循环相比上面几种做法,有一些显著的优点。
- 有着同for…in一样的简洁语法,但是没有for…in那些缺点。
- 不同于forEach方法,它可以与break、continue和return配合使用。
- 提供了遍历所有数据结构的统一操作接口。