为什么for...of...无法改变元素值

1,078 阅读4分钟

前言

以前使用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标准入门》