前言
以前使用for...of...语句的时候只是想当然地当作一种遍历方式来使用,后来遇到了三个并加以思考问题:
问题1
let array = [1, 1, 1, 1, 1, 1];
for(let item of array){
item = 2;
}
console.log(array); //[1, 1, 1, 1, 1]
此代码运行过后,array数组的内容并不会改变。
如果元素是对象,其属性值就可以改变。
let objectArray = [
{value:1 },
{value:1 },
{value:1 }
];
for(let item of objectArray){
item.value = 2;
}
console.log(objectArray);
问题2
为什么Object对象无法使用for...of...
问题3
为什么遍历Map的时候是返回数组[key, value]。
let map = new Map();
map.set("a", 1);
map.set("b", 2);
map.set("c", 3);
for(let item of map){
console.log(item); //依次输出:["a", 1], ["b", 2], ["c", 3]
}
//也可如下方式遍历
for(let [key, value] of map){
console.log(key); //依次输出:"a", "b", "c"
console.log(value); //依次输出:1, 2, 3
}
简要地说,这几个问题的根本原因是for...of...是利用遍历器(Iterator)来进行遍历的,后文将先介绍遍历器,再对其解答
本文内容学习自阮一峰的ECMAScript 6 入门——Iterator 和 for...of 循环,并根据动手试验加以总结。
遍历器本质
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。
——摘自《ECMAScript 6 入门——Iterator 和 for...of 循环》
简而言之,遍历器即为遍历数据结构的接口,每次调用接口其指针会返回对应数据,并指向下一个数据,直到该数据结构遍历完,
这里先用js模拟一个遍历器,以了解遍历器的作用(注意,是模拟!):
function createIterator(dataArray){
let nextIndex = 0;
return {
next:function(){
return nextIndex < dataArray.length?
{value: dataArray[nextIndex++], done:false}:
{value: undefined, done:true}
}
}
}
let iterator = createIterator(['a','b']);
console.log(iterator); //{next:f} 遍历器对象,含有next方法
iterator.next(); // {value: "a", done:false}
iterator.next(); // {value: "b", done:false}
iterator.next(); // {value: undefined, done:true}
createIterator(dataArray)返回一个遍历器,其中利用了闭包的特性来维护指针nextIndex。每次遍历器调用next()方法,都会返回对应一个对象,其中包含value属性与done属性,分别表示值与其是否遍历结束。
ES6默认的Iterator接口
ES6中默认的Iterator接口部署在数据结构的Symbol.iterator属性,也即是说ES6中一个数据结构具有Symbol.iterator属性,其就是可遍历的,可以使用for...of...进行遍历。
我们可以先获取数组默认的Iterator接口,然后把它应用上去看看:
const dataArray = ['a', 'b', 'c'];
const makeIterator = dataArray[Symbol.iterator];
const iterator = makeIterator.call(dataArray);
console.log(iterator);// Array Iterator {}
iterator.next(); //{value: "a", done: false}
iterator.next(); //{value: "b", done: false}
iterator.next(); //{value: "c", done: false}
iterator.next(); //{value: undefined, done: true}
自己编写一个Iterator接口
了解了ES6默认的Iterator接口部署在哪,我们可以编写object的iterator接口,这样此object就是可遍历对象了。
这里用一个类数组对象进行测试
let iterObj = {
0: "a",
1: "b",
2: "c",
length: 3,
[Symbol.iterator]: function () {
let index = 0;
const next = () => {
if (index < this.length) {
return {
done: false,
value: this[index++],
};
} else {
return {
done: true,
value: undefined,
};
}
};
const iter = { next: next };
return iter; //返回一个带有next方法的遍历器用于遍历,利用了闭包的性质
},
};
const iter = iterObj[Symbol.iterator]();
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
console.log(iter.next());
for(let item of iterObj){
console.log(item);
}
如此就可以通过for...of...遍历对象了。
回答上面的三个问题
经过以上了解与实践,回答上面三个问题就非常非常简单了。
接下来一一说明
问题一
以下for...of...中item其实是每次遍历器next()方法return的值,如果数组元素是基本类型,改变函数return的值当然对原数组无影响。
let array = [1, 1, 1, 1, 1, 1];
for(let item of array){
item = 2;
}
console.log(array); //[1, 1, 1, 1, 1]
而如果是数组元素是引用类型,遍历器next()方法return的是对元素的引用,那么改变对象的属性值就会发生变化。
let objectArray = [
{value:1 },
{value:1 },
{value:1 }
];
for(let item of objectArray){
item.value = 2;
}
console.log(objectArray);
这里只要了解for...of...是利用遍历器的本质及js的基本类型与引用类型,就知道原因了
问题二
原生的Object对象不含有Symbol.iterator属性,所以无法for...of...无法获得遍历器用于遍历。
问题三
Map的遍历器next方法()返回的value是一个二维数组,内容即为[key, value]。所以另一种写法只不过是解构赋值罢了
for(const [key, value] of map){//解构赋值
//...
//...
}
结语
以上问题主要来源于偶然使用for...of...改变数组元素值,想当然以为是普通的遍历方式,不知其原来是遍历器,了解相关内容后也就不难理解缘由了。
本文学习内容很多参考了阮一峰的《ES6标准入门》并加上一些实践与总结,更详细的内容可看其原书。
参考资料
《ES6标准入门》