目录
Iterator 的概念
JavaScript 表示“集合”的数据结构,主要有:Array、Object、Map、Set。
遍历器(Iterator),为各种不同的机制提供统一的访问机制。任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 作用有三个:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构中每个成员能够按照某种次序排列;
- ES6 创建了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。
Iterator 的遍历过程
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上是一个指针对象。
- 第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员。
- 第二次调用,就指向数据结构的第二个成员。
- 不断的调用指针的 next 方法,直至它指向数据结构的结束位置。
每一次调用 next 方法,都会返回数据结构的当前成员信息。具体来说就是返回一个包含 value 和 done 两个属性的对象。其中 value 属性是当前成员的值,done 是一个布尔值表示遍历是否结束。
模拟 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) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
// makeIterator 其实就是返回了一个对象,这个对象有 next 方法,next 方法每次返回数组的下一项。
// next 方法还是一个闭包,每次访问的都是同一个 nextIndex,所以可以实现依次逐个访问数组每一项
// 我们自己可以随便写一个这样的方法,比如倒着遍历...
对于遍历器来说,done: false 和 value: undefined 都是可以省略的(也就是说 done 默认是 false,value 默认是 undefined)。所以上面函数可以简写:
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++]} :
{done: true};
}
};
}
默认 Iterator 接口
当用 for...of 循环遍历某种数据结构时,该循环就会自动去找 Iterator 接口。
一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”。
ES6 规定:默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可遍历的”。Symbol.iterator 属性本身是一个函数,就是当前数据结构的默认遍历器生成函数。执行这个函数就会返回一个遍历器。至于属性名 Symbol.Iterator,它是一个表达式,返回 Symbol 对象的 Iterator 属性,这是一个预定义好的、类型为 Symbol 的特殊值。
对象的 Symbol.iterator 属性,指向该对象的默认遍历器方法。
注意:
let obj = { a: 1, b: 2 };
console.log(obj[Symbol.iterator]);
// undefined
可以看到对象其实并没有 Symbol.iterator 属性。可遍历也只是自己写的遍历方法。
写一个可遍历的对象示例:
const obj = {
[Symbol.iterator]: function () {
return {
next: function () {
return {value: 1, done: true};
}
};
}
};
for (var i of obj) {
console.log(i); // 无输出...
}
解读:对象 obj 是可遍历的(iterable),因为具有 Symbol.iterator 属性。执行这个属性,就会返回一个遍历器对象,该对象的根本特征就是具有 next 方法。每次调用 next 方法,都会返回一个代表当前成员的信息对象,具有 value 和 done 属性。
ES6 有些数据结构原生具备 Iterator 接口,即不用做任何处理,就可以被 for...of 循环遍历。原因在于这些数据结构原生部署了 Symbol.iterator 属性。凡是部署了 Symbol.iterator 属性的数据结构,就称为部署了遍历器接口,调用这个接口就会返回一个遍历器对象。
原生具备 Iterator 接口的数据结构
- Array
- Map
- Set
- String
- 函数的 arguments 对象
- TypedArray
- NodeList 对象
数组的 Symbol.iterator 属性
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 }
对于原生部署的 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of 循环会自动遍历它们。其他数据结构(比如对象)的 Iterator 接口,需要自己在 Symbol.iterator 属性上面部署,这样才会被 for...of 循环遍历。
对象之所以没有部署 Iterator 接口,是因为对象的哪个属性先遍历哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署了一种线性转换。
给一个对象部署遍历器接口 ------ 在 Symbol.Iterator 的属性上部署遍历器生成方法
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var 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);
}
range(0, 3) // RangeIterator {value: 0, stop: 3}
for (var val of range(0, 3)) {
console.log(val ); // 0, 1, 2
}
解读(个人理解):
for...of 循环内部调用的是数据结构的 Symbol.iterator 方法,拿到返回的遍历器对象,在调用遍历器对象的 next 方法;for (var variable of iterable) {}:variable:当前成员的 value 值,iterable:部署的 Iterator 接口的数据结构。
new RangeIterator(start, stop) 调用构造函数,返回 this(构造函数的返回值如果是引用类型就返回这个引用,如果不是或者没有,就返回 new 创建的实例;在调用构造函数时,这实例指向了 this);
第一次循环,调用 Symbol.iterator 方法返回 this (遍历器对象),再调用这个遍历器对象的 next 方法,返回第一个成员 {done: false, value: 1}(此时 this.value 已经++),将该成员的 value 值赋给 val;
第二次循环调用 next 方法,返回第二个成员...
直至 done 为 true 停止。
var opr = range(0, 3)[Symbol.iterator]();
console.log(opr.next());
console.log(opr.next());
console.log(opr.next());
console.log(opr.next());
依次打印:
{done: false, value: 0}
{done: false, value: 1}
{done: false, value: 2}
{done: true, value: undefined}
for...of 先调用 Symbol.iterator 方法返回遍历器对象,再用这个对象依次调用 next 方法并将返回值的 value 值赋值给 val(此时 done 属性值必为 false),实现遍历(done 为 true 时应该是自动截止了并且不会将此时的值赋值给 val)。
有一点是必须的,就是返回值对象必须包含 value 属性和 done 属性(若没有还用 for...of 遍历可能会陷入死循环)
通过遍历器实现指针结构的例子:
function Obj(value) {
this.value = value;
this.next = null;
}
Obj.prototype[Symbol.iterator] = function() {
var iterator = { next: next };
var current = this;
function next() {
if (current) {
var value = current.value;
current = current.next;
return { done: false, value: value };
} else {
return { done: true };
}
}
return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;
for (var i of one){
console.log(i); // 1, 2, 3
}
解读(个人理解):
one、two、three 三个变量分别为:
one { value: 1, next: two }
two { value: 2, next: three }
three { value: 3, next: null }
for...of
第一次调用 one 实例的 Symbol.iterator 方法:
此时 this 指针指向 one 这个实例
很明显返回 { done: false, value: 1 }
再返回之前,有个关键点:current = current.next; 在这里,this 指针的指向改变,指向了 current.next
current.next == this.next == one.next == two(实例)
当第二次调用 next 方法时,this 指向 two 实例,value 值为 2,同时再次改变 this 指针指向
调用过程:
var opr = one[Symbol.iterator]();
console.log(opr.next()); // {done: false, value: 1}
console.log(opr.next()); // {done: false, value: 2}
console.log(opr.next()); // {done: false, value: 3}
另一个为对象添加 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 };
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (var k of obj) {
console.log(k);
}
// hello
// world
解读:
最开始以为 for...of 内部都是这样调用:
console.log(obj[Symbol.iterator]().next());
console.log(obj[Symbol.iterator]().next());
但是仔细一想加上一打印发现,打印的都是 {value: "hello", done: false}
所以上面的一些想法应该错了...(目前上面已改)
可能是这样:
var opr = obj[Symbol.iterator]();
console.log(opr.next());
console.log(opr.next());
也就是,for...of 第一次循环会调用 Symbol.iterator,然后将返回值(遍历器对象)保存,每次遍历都直接用这个返回值来调用 next() 方法
对于类似数组的对象(存在数值键名和 length 属性),部署 Iterator 接口的简便方法,将数组的 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 方法对应的不是遍历器生成函数(即不会返回一个遍历器对象),则解释引擎会报错。
var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj] // TypeError: [] is not a function
有了遍历器接口,数据接口就可以用 for...of 循环遍历,也可以用 while 循环。
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 };
} else {
return { value: undefined, done: true };
}
}
};
}
};
var $iterator = obj[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
var x = $result.value;
console.log(x);
$result = $iterator.next();
}
// hello
// world
上面代码中,obj 是可遍历的对象,$iterator 是它的遍历器对象。遍历器对象每次移动指针(next 方法),都会检查一下返回值的 done 属性,如果遍历还没结束,就移动遍历器的指针到下一步(next 方法),不断循环。
(看到这里,上面自己的推测好像是对的...^_^)
默认调用 Iterator 接口的场合
1)解构赋值
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'];
2)扩展运算符
// 例一
var str = 'hello';
[...str] // ['h', 'e', 'l', 'l', 'o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
可以将任何部署了 Iterator 接口的数据结构,转为数组。
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)其他场合
- for...of
- Array.from()
- Promise.all()
- Promise.race()
字符串的 Iterator 接口
var someString = "hi";
typeof someString[Symbol.iterator] // "function"
var iterator = someString[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() // { value: undefined, done: true }
可以覆盖字符串的 Symbol.iterator 方法
var str = new String('world');
str[Symbol.iterator] = function () {
var flag = true;
return {
next() {
if (flag) {
flag = false;
return { value: 'hello', done: false };
} else {
return { done: true }
}
}
};
}
console.log([...str] + ''); // hello
console.log(str.toString()); // world
Iterator 接口与 Generator 函数
Symbol.iterator 最简单实现
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"
遍历器对象的 return()、throw()
遍历器对象除了具有 next() 方法,还可以具有 return() 方法和 throw() 方法。自己写遍历器对象生成函数,next() 是必须部署的,其它是可选的。
return() 方法的使用场景是,如果 for...of 循环提前退出(出错或者有 break),就会调用 return()。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署 return() 方法。
function readLinesSync(iterator) {
return {
[Symbol.iterator]() {
return {
next() {
return { done: false };
},
return() {
return { done: true };
}
};
},
};
}
会触发 return() 的情况
// 情况一
for (let val of iterater) {
console.log(val);
break;
}
// 情况二
for (let val of iterater) {
console.log(val);
throw new Error();
}
注意:return() 方法必须返回一个对象,这是 Generator 语法决定的。
数组中的 for...in 和 for...of
let arr = [3, 5, 7];
arr.foo = 'hello';
for (let i in arr) {
console.log(i); // "0", "1", "2", "foo"
}
for (let i of arr) {
console.log(i); // "3", "5", "7"
}
for...of 循环数组,只返回具有数字索引的属性。
对象中使用 for...of
对于普通的对象,for...of 不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。
var obj = { a: 1, b: 2 };
obj[Symbol.iterator] = function () {
let keys = Object.keys(obj);
let len = keys.length;
let that_ = this;
let index = 0;
return {
next() {
if (index < len) {
var val = that_[keys[index]];
index++;
return { value: val, done: false };
} else {
return { done: true };
}
}
}
}
for (var val of obj) {
console.log(val);
}
// 1 2
let obj = {a: 1, b: 2};
for (var key of Object.keys(obj)) {
console.log(key + ': ' + obj[key]);
}
// 使用 Generator 函数将对象重新包装
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
与其他遍历语法比较
(偷下懒...v_v)