ES6中forEach、map等方法会修改原数组吗?

实际上不止数组的forEach、map函数会出现这种问题,数组的其他方法如:find、findIndex、filter、every、some这些函数都会出现这种问题。这些函数的特点是会遍历数组,对每个数组元素都执行一次传入的回调方法。

日常先上结论:

1、用forEach、map函数对引用类型的数组元素的属性值进行了修改,原数组也会跟着改变。
2、如果你不希望原数组在上述情况下被改变的解决办法:
  对操作数组进行深拷贝。用拷贝的对象调用数组处理方法,原数组就不会改变了。

我曾在实习的时候使用forEach,map函数去处理一个数组,然后发现原数组竟然被修改了,导致其他引用原数组的代码乱了套。

我们看下面这段代码,发现原数组确实被改变了:

var people = [{name: "zhangsan"}, {name: "lisi"}, {name: "wangwu"}];
people.forEach((item) => {
    item.name = "xxx"
});
console.log(people);
// 打印出[{name: "xxx"}, {name: "xxx"}, {name: "xxx"}]

欸?不对啊,我记得forEach、map这些数组方法貌似都不会更改原数组啊,难道我记错了?
我们来看MDN对forEach的使用描述:

这解释就很官方,不会改变原数组,但是又有可能会改变数组,有一种懂的人都懂,不懂的人就不会懂的感觉。forEach、map这类函数的设计初衷也是不希望他会修改原数组,更多是希望通过传入回调对原数组元素进行修改返回一个新数组以供特殊使用(forEach不会返回新数组)。但是如上面我结论所说去调用这些方法依然会改变原数组,原因不是三言两语能解释的清,限于篇幅MDN只能如此描述。(我一直觉得官方文档都不太适合新手看,新手更应该看一些博客,人讲出来的话更为容易理解,比如说看我的博客,哈哈哈!)

那么哪些情况下使用forEach会改变原数组呢?要满足两个条件
  1、你要操作的那些数组元素是一个引用类型的数据,即可能是数组、对象、函数(函数是引用类型)。
  2、forEach里的回调函数对引用类型的数组元素的属性有修改操作。
  上面两条总结一下就是,还是上面的例子,你是直接修改整个元素,而不是修改该元素的某个属性,那么原数组也不会被改变。

var people = [{name: "zhangsan"}, {name: "lisi"}, {name: "wangwu"}];
people.forEach((item) => {
    item = {name: "xxx"};
});
console.log(people);
// 打印出[{name: "zhangsan"}, {name: "lisi"}, {name: "wangwu"}]

以上讲解了在哪些情况下forEach、map会修改原数组。那么为什么修改引用类型的元素里面的属性就会修改原数组呢?
  主要原因是数组元素他是引用数据类型的。这就谈到引用数据类型和基本数据类型的区别。要解释清楚有点难,涉及到内存的一些知识。

先说明js的运行环境是浏览器,浏览器运行会向系统申请一块内存来使用。此时他会将申请的这块内存的一部分用来用来存储对象,叫堆区。还有一部分用来存储对象的引用和一些基本数据类型(也叫字面量值),叫栈区,因为它对这块内存的使用符合栈的数据结构特点,所以叫栈区。栈的数据结构特点:1、线性一维的。2、只能在一端进行插入和删除操作,按照“先进后出”的原则存储数据。

JS中的数据类型大致可以分为两类
  1、一种是基本数据类型,包括string,number,boolean等。
  基本数据类型存放在栈中,以键值对的形式存放。具体如图所示。基本数据类型在栈里是按值访问的。基本数据类型的复制是值的复制,复制后两者互相独立。

2、另一种是引用数据类型,包括对象、数组和函数。
  引用数据类型存放在堆中。他是动态分配的,在底层上面来说,就是他这里的内存是通过一个类似C的malloc()函数来向系统申请存储一个新的对象的内存空间。为什么是动态分配的?因为一个对象,它占用的空间是不确定的。对象不知道里面包含多少个属性,所以需要在创建对象时根据情况动态向系统申请需要占用的内存。
  学过C的应该知道,C的malloc()分配空间的时候返回的是内存分配的首地址。这个首地址赋值给谁呢?赋值给栈里的对应的对象名。也就是创建一个对象,他的对象数据存储在堆里,他的变量名存储在栈中,变量名的值是该对象的引用。对象在栈里是按引用访问。
  在C里面,当你使用指针去操作一个数据时,因为他是通过指针找到该地址所存储的数据,你修改指针指向的内存地址里的数据,那么该变量指向的那块内存里的数据就改变了。所有其他引用这个地址的变量的值也会随之被改变。他不像基本数据类型那样,如果值改变了,他就直接修改栈里变量对应的值就好了。
  对象的复制是引用的复制。即一个对象赋值给另一个变量名时,他们此时指向的内存地址是一样的。因此他们指向的都是同一个东西。而对象的修改,因为对象名的值是指针类型,他一直指向该对象存储在堆里的首地址,所以你对他的修改,其他引用这个对象的变量的值也会随之修改。
  另外你每次创建一个新的对象,不管对象的值是否相等,他都会重新开辟一块内存来存储这个新创建的对象。这也是为什么[] == [],{} == {}都是false。因为当==比较的数据是引用类型的数据时,他比较的是两个对象的引用,即他们的内存地址是否相同。因为他们是内容相同的两个不同的对象,内存地址不同,所以为false。

好了,如果前面你看懂了,那么就知道为什么在上述的情况下,forEach会修改原数组了。因为forEach对原数组进行了一个简单的浅拷贝。从下面我对深拷贝和浅拷贝的解释,可以看出浅拷贝的对象的改变原对象也会改变,深拷贝的对象的改变与原对象无关,原对象不会改变。所以为了让上述情况下,原数组不会被改变,我们需要创建这个数组对象的深拷贝,然后用这个深拷贝数组对象去调用ES6的数组处理方法。

// 以map举例,他的内部大概实现,其他ES6新增数组处理函数也差不多
Array.prototyope.map = function(callback){
    // this指向调用map函数的那个数组
    var tempArr = this;
    // 调用回调函数处理tempArr,因为数组元素是引用类型,所以修改引用类型的数据原数组也会被更改
    // 这里的操作根据不同的ES6数组处理方法,调用它的回调函数;
    for(var i = 0; i < tempArr.length; i++){
        callback(tempArr[i]);
    }
    // 如果是forEach这些不需要返回数组的处理方法,不需要这步
    return tempArr;
}

结合上下这两段代码,解释了为什么直接整个修改引用类型数组元素,不会修改原数组,只有修改引用类型的数组元素的属性才会修改原数组。

var people = [{name: "zhangsan"}];
people.forEach((item) => {
    // 这里item是将item整个替换成{name: "xxx"},没有对item指向的那个对象有操作
    // 相当于item换了一个指向,指向其他内存地址了,没有修改到原数组。
    item = {name: "xxx"};
    // 这里因为item是一个对象,他跟people指向同一个内存地址,所以item里面的属性值,原数组也改变了
    item.name = "xxx"
});
console.log(people);

什么是深拷贝、什么是浅拷贝?
  深拷贝就是即使变量的值相同,但是变量指向的内存地址不相同,互相独立。
这里b是a的深拷贝

var a = {name: "a"}
var b = JSON.parse(JSON.stringify(a));
console.log(a, b); // {name: "a"} {name: "a"}
b.name = "b";
console.log(a, b); // {name: "a"} {name: "b"}

浅拷贝就是尽管他们的变量名不相同,但是他们指向的内存地址相同,互相影响。
这里b就是a的浅拷贝。

var a = {name: "a"}
var b = a;
console.log(a, b); // {name: "a"} {name: "a"}
b.name = "b";
console.log(a, b); // {name: "b"} {name: "b"}
// 这里注意一点, 这里直接修改b这个变量,系统会将b的指针指向常量区存有4的内存地址
// 而不是将b之前指向a的内存地址里的值改为4
b = 4;
console.log(a, b); // {name: "b"} 4

最后再来说一下,js怎么实现深拷贝和浅拷贝
  1、深拷贝的话,简单实现可以用到JSON的序列化和反序列化。这个对数组对象都有用。

var a = {name: "a"}
var b = JSON.parse(JSON.stringify(a));
1
2
  复杂实现需要定义一个函数,传入要深拷贝的对象,即可返回一个深拷贝对象。但是这个函数只能深拷贝引用类型的数据。

function  deepClone(obj){
    if(typeof obj !== 'object') return;
    var newObj = obj instanceof Array?[]:{};
    // for in循环
    for(var i in  obj){
        // 忽略继承属性
        if(obj.hasOwnProperty(i)){
            newObj[i] = typeof obj[i] === 'object'?deepClone(obj[i]) : obj[i];
        }
    }
    return newObj;
}

基本数据类型的深拷贝就不需要考虑了,本来就互相独立。

2、浅拷贝的话就比较简单了,创建一个数据类型的对象,用另一个变量引用他就好了

var obj = {};
var obj1 = obj;

复杂一点的代码实现

function  deepClone(obj){
    if(typeof obj !== 'object') return;
    var newObj = obj instanceof Array?[]:{};
    // for in循环
    for(var i in  obj){
        // 忽略继承属性
        if(obj.hasOwnProperty(i)){
            // 这里直接复制就是浅拷贝啦
            newObj[i] = obj[i];
        }
    }
    return newObj;
}

以上就是ES6新增数组处理函数会修改原函数的情况及其原因。还有解决此种问题的办法,使用深拷贝。下面是解决该问题的代码实例

var people = [{name: "zhangsan"}, {name: "lisi"}, {name: "wangwu"}];
var people1 = JSON.parse(JSON.stringify(people));
people1.forEach((item) => {
    item.name = "xxx"
});
console.log(people,people1);
// people: [{name: "zhangsan"}, {name: "lisi"}, {name: "wangwu"}]
// people1: [{name: "xxx"}, {name: "xxx"}, {name: "xxx"}

日常再总结一遍:

1、用ES6新增数组函数修改引用类型的元素里面的属性原数组也会跟着改变。
2、如果你不希望原数组被改变解决办法:
对操作数组进行深拷贝。用拷贝的对象调用数组处理方法,原数组就不会改变了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容