实际上不止数组的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、如果你不希望原数组被改变解决办法:
对操作数组进行深拷贝。用拷贝的对象调用数组处理方法,原数组就不会改变了。