1、用ES6新增数组函数修改引用类型的元素里面的属性原数组也会跟着改变。
2、如果你不希望原数组被改变解决办法:
对操作数组进行深拷贝。用拷贝的对象调用数组处理方法,原数组就不会改变了。
我们看下面这段代码,发现原数组确实被改变了:
var people = [{name: "zhangsan"}, {name: "lisi"}, {name: "wangwu"}];
people.forEach((item) => {
item.name = "xxx"
});
console.log(people);
// 打印出[{name: "xxx"}, {name: "xxx"}, {name: "xxx"}]
欸?不对啊,我记得ES6新增的数组方法貌似都不会修改原数组啊,难道我记错了?
我们来看MDN对forEach的使用描述:
这解释就很官方,不会改变原数组,但是又有可能会改变数组,有一种懂的人都懂,不懂的人就不会动的感觉。MDN也没办法,forEach的设计初衷也是不希望他会修改原数组,但是在某些特殊情况下又可以修改,限于篇幅,他只能这么描述。(我一直觉得官方文档都不太适合新手看,新手更应该看一些博客,人讲出来的话更为容易理解,比如说看我的博客,哈哈哈!)
1、你要操作的那些数组元素是一个引用类型的数据,即可能是数组、对象、函数(函数是引用类型,并且也可以放到数组里面去,试了一下满足条件,函数元素也会被改变)。
2、forEach里的回调函数对引用类型的数组元素的属性有修改操作。
就是使用ES6新增数组函数时,修改了引用类型的元素里面的属性原数组也会跟着改变。
上面两条总结一下就是,还是上面的例子,你是直接修改整个元素,而不是修改该元素的某个属性,那么原数组也不会被改变。
var people = [{name: "zhangsan"}, {name: "lisi"}, {name: "wangwu"}];
people.forEach((item) => {
item = {name: "xxx"};
});
console.log(people);
// 打印出[{name: "zhangsan"}, {name: "lisi"}, {name: "wangwu"}]
主要原因是数组元素他是引用数据类型的。这就谈到引用数据类型和基本数据类型的区别。要解释清楚有点难,涉及到内存的一些知识。
先说明js的运行环境是游览器,游览器运行会向系统申请一块内存来使用。此时他会将申请的这块内存的一部分用来用来存储对象,叫堆区,为什么叫堆?因为他对这块内存的使用符合堆的数据结构特点。堆的数据机构特点是:1、堆是一颗完全二叉树。2、他的某个节点总是不大于或不小于他的父节点。还有一部分用来存储对象的引用和一些基本数据类型(也叫字面量值),叫栈区,同样因为它对这块内存的使用符合栈的数据结构特点,所以叫栈区。栈的数据结构特点: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
1、深拷贝的话,简单实现可以用到JSON的序列化和反序列化。这个对数组对象都有用。
var a = {name: "a"}
var b = JSON.parse(JSON.stringify(a));
复杂实现需要定义一个函数,传入要深拷贝的对象,即可返回一个深拷贝对象。但是这个函数只能深拷贝引用类型的数据。
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;
}
基本数据类型的浅拷贝,可以将基本数据类型转为引用数据类型。
// 这里m就是n的浅拷贝,str1就是str的浅拷贝。
var n = new Number(123);
var m = n;
var str = new String("abc");
var str1 = str;
以上就是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"}
我先讲一下java的栈,因为我之前学习过java,也了解过Java的栈。java的栈对对象的存储与js基本一致,但是对基本数据类型的存储与我上述不太一样。
JAVA基本数据类型的存储也是在栈区,也是以键值对的形式存储。不同的是,java栈里的键是变量名,键对应的值不管你是引用数据类型还是基本数据类型都是引用。如果有一个变量a,他的值是4,那么java就会去他的栈区里找是否已经存有4这个常量,如果有,a就指向存有4的那个内存地址,如果没有就自己在栈区找一个位置存放4这个常量,然后让a指向这里。栈区不可能存在两块内存,保存相同的常量。也就是说当有其他变量也等于4时,即b=4,那么b也指向这里,即b和a指向同一个内存地址。假设此时b=5了,b不会修改b指向的内存里面的值,也不会销毁它,如果这么做了,其他变量对常量4的引用就会失效。同样,b=5时它会继续在栈区里面找有没有存5这个常量,如果没有就找一个地方存5,然后b指向这里,如果已经存在5这个常量了,b直接指向常量区存有5的内存地址。
这样无疑能节省一部分空间,我原本理解js栈区存储基本数据类型也是同java一样,但是我查看了一些其他博客,他们都认为栈区基本数据类型保存的不是引用,而是直接保存他们的值。希望有人能评论指出我的疑惑。
1、用ES6新增数组函数修改引用类型的元素里面的属性原数组也会跟着改变。
2、如果你不希望原数组被改变解决办法:
对操作数组进行深拷贝。用拷贝的对象调用数组处理方法,原数组就不会改变了。
大家能看到结尾很不容易,我能写这么长也很不容易,如此详细,有收获的点个赞、评论一下呗,祝大家身体安康。(至今没有人给我点赞、评论)。因为比较偏底层,有一些是我自己的理解,如果有错误请及时指出。