前言
最近做项目遇到一个问题,后台没有做查看列表详情的接口,于是乎只能从已获取的数据里面查找展示。但是问题来了,在一次编辑的时候,把手机号清空之后,就关了dialog,并没有修改数据。按照正常思维,此时列表中的数据应该不变的,但是邪门的事情出现了,刚刚编辑的那行数据的手机号不见了!!天啦,怎么回事,我也没有提交啊。经过再三思索,这可能和引用类型的拷贝有关系。预知后事如何,请往下看。
堆和栈与数据类型
堆和栈
堆和栈都是内存中用于存储的区域。
栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。
数据类型
数据类型分为基本数据类型和引用数据类型,其中,基本数据类型有:null、undefined、string、boolean、number、Symbol,引用数据类型主要就是对象和数组。
- 基本数据类型的特点:直接在栈中存储。
- 引用数据类型的特点:实体存放在堆内存中,栈中存储的是指向其实体的指针。

基本数据类型
- 值不可变
javascript中的原始值(undefined、null、布尔值、数字和字符串)与对象(包括数组和函数)有着根本区别。原始值是不可更改的:任何方法都无法更改(或“突变”)一个原始值。对数字和布尔值来说显然如此 —— 改变数字的值本身就说不通,而对字符串来说就不那么明显了,因为字符串看起来像由字符组成的数组,我们期望可以通过指定索引来假改字符串中的字符。实际上,javascript 是禁止这样做的。字符串中所有的方法看上去返回了一个修改后的字符串,实际上返回的是一个新的字符串值。
let str = 'hello';
str[0] = 'H';
conosle.log(str); // => hello
- 比较的是值
基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的:
let a = 1;
let b = 1;
console.log(a === b); // => true
引用类型
- 引用类型值可变
let obj = {
name: 'jack',
age: 23
};
obj.name = 'ace';
console.log(obj); // => {name: "ace", age: 23}
- 比较是堆地址的比较
引用类型比较的时候,比较的是其保存的堆指针是否指向同一个对象:
let a = [1,2,3];
let b = [1,2,3];
console.log(a === b); // => false
尽管变量a和变量b都是表示一个内容为1,2,3的数组,但是他们指向的并不是同一个数组。
传值与传址了解一下?
在进行赋值操作的时候,基本数据类型的赋值其实是在栈内存中开启一段内存,将赋值的值存入新栈中:
let a = 10;
let b = a;
a = a*2;
console.log(a); // => 20
console.log(b); // => 10

而引用类型的赋值是传址。改变的是指针的指向:
let a = {}; // a保存了一个空对象实例
let b = a; // 将a赋值给b,其实是将a保存的指针指向赋给b
a.name = 'jack';
console.log(a.name); // => jack
console.log(b.name); // => jack
b.age = 22;
console.log(a.age); // => 22
console.log(a.age); // => 22
console.log(a === b); // => true
其实,a和b指向的是同一个对象,不管是操作a还是b,都是在操作同一个对象。

切入正题:赋值与浅拷贝与深拷贝
赋值
上面说到,引用类型的赋值其实是传址,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
浅拷贝
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
浅拷贝的方式
Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
let a = {
name: 'jack',
age: 22,
girlFriends:{
first: 'lili',
second: 'lisa',
third: 'alice'
}
}
let obj = Object.assign({},a);
a.girlFriends.first = 'bill';
a.age = 21;
console.log(obj);

Array.prototype.concat()
let arr = [1,32,{name: 'jack'}];
let newArr = arr.concat();
arr[2].name = 'ace';
console.log(newArr);

Array.prototype.slice()
let arr = [1,32,{name: 'jack'}];
let newArr = arr.slice();
arr[2].name = 'ace';
console.log(newArr);

原数组的元素会按照下述规则拷贝:
-
如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
-
对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
深拷贝
深拷贝就是对对象,已经对象所有的子对象进行拷贝。 参考一下浅拷贝,如果抛去例子中的子对象,以上三种浅拷贝其实都可以看做是一个深拷贝,那么答案显而易见:递归!
深拷贝的实现方法
JSON.parse(JSON.stringify())
这个方法大家应该都用过,简单粗暴:
let arr = [1,2,{name:'jack'}];
let newArr = JSON.parse(JSON.stringify(arr));
arrr[2].name = 'ace';
console.log(newArr);

原理:用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数
这是因为JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数。
不过JSON.stringify()提供了第二个参数:replacer
如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化
let obj = {
hello: function(){
console.log('hello');
}
};
function replacer(key,val){
if(typeof this[key] === 'function'){
return this[key].toString();
}
return val;
}
console.log(JSON.stringify(obj,replacer));

手写递归
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
首先,我们写一个简单的遍历对象的浅拷贝:
let obj = {
name: 'jack',
age: 23,
girlFriends:{
first: 'lili',
second: 'alice'
}
};
function clone(obj){
let newObj = {};
for(let i in obj){
if(obj.hasOwnProperty(i)){
newObj[i] = obj[i]
}
}
return newObj;
}
let newObj = clone(obj);
obj.girlFriends.first = 'lisa';
console.log(obj);
console.log(newObj);

可以看到,obj和newObj对象中,girlFriends属性所指向的对象仍然是同一个,这时我们需要对clone函数进行递归改写:
function clone(obj){
let newObj = {};
for(let i in obj){
if(obj.hasOwnProperty(i)){
if(typeof obj[i] === "object"){
newObj[i] = clone(obj[i])
}else{
newObj[i] = obj[i]
}
}
}
return newObj;
}

然鹅,引用类型可不止一个,我们得考虑一下拷贝对象是数组的情况,继续改造:
let obj = {
name: 'jack',
age: 23,
girlFriends:{
first: 'lili',
second: 'alice'
},
hobbies: ['bike','ball','swimming']
};
function clone(obj){
let newObj = Array.isArray(obj)?[]:{};
for(let i in obj){
if(obj.hasOwnProperty(i)){
if(typeof obj[i] === "object"){
newObj[i] = clone(obj[i])
}else{
newObj[i] = obj[i]
}
}
}
return newObj;
}
let newObj = clone(obj);
obj.girlFriends.first = 'lisa';
obj.hobbies[1] = 'what?';
console.log(obj);
console.log(newObj);

至此,一个简易版的深拷贝函数就完成了,然鹅,事实并没有辣么简单,比如:
- 如果对象引用自身的话会出现什么情况?死循环?
- for in循环能否用其他循环改写,性能如何?
- 对引用类型的合理判断,如果是其他引用类型如何处理等。。
后记
说到最后,其实博主就是一战五都没有的渣,以上内容仅供参考,如果有发现问题请在评论区留言,理性发言,文明观球。
参考资料
- 如何写出一个惊艳面试官的深拷贝?(强烈建议阅读)
- js 深拷贝 vs 浅拷贝(对于原理性的解释很生动详细)
- 浅拷贝与深拷贝(反正写的就是比我好)