一、引言
在js中,数据分为基础类型和引用类型两大类:
6种基础类型:
-
number类型
-
string类型
-
boolean类型
-
symbol类型
-
null类型
-
undefined类型
1种引用类型:
- object类型
对于基础类型的数据,其大小是固定不变的,所以变量名和值都存储在栈内存中的。对于引用类型的数据,其大小可能会变化,所以变量名和数据在堆内存中的地址存储在栈内存,数据本身存储在堆内存中。
let a = 10
let obj1 = {
m: 111
}
对于上面两个变量,在内存中是以下面这种方式存放的:
可以看出变量a的值是基础类型,所以直接存在了栈内存中,而obj1的值是引用类型,它在栈内存中的值是一个地址(地址是编的,不是真实地址值),指向堆内存中存储的对象本身。
如果我们分别把变量a和obj1复制一份
let a = 10
let b = a;
let obj1 = {
m: 111
}
let obj2 = obj1;
那么原始变量和复制后的变量在内存中的结构就是下面这种:
出现这种现象的原因是因为,对于基础类型,=号的操作是值传递操作,直接把a的值复制一份给b,而对于引用类型,=号的操作是址传递操作,obj1把自己的堆内存地址复制了一份给obj2,所以obj1和obj2指向的是堆内存中的同一个数据。
这个时候我们如果修改原始变量的值,基础类型的不会受影响,而引用类型却会被影响:
let a = 10
let b = a;
a = 20;
console.log(a); // 20
console.log(b); // 10
let obj1 = {
m: 111
}
let obj2 = obj1;
obj1.m = 222;
console.log(obj1.m); // 222
console.log(obj2.m); // 222
这种互相影响是我们不想要的,我们想要的复制是obj1和obj2在内存中是分开存储的,就像下面这种:
这种情况下,obj1和obj2是两个独立的数据,不会互相影响。要实现这种复制,就需要浅拷贝和深拷贝了。
对于基础数据来说,=号操作符就是值传递,会在栈内存中分别保存,不存在深浅拷贝的说法,只有引用类型数据才有深浅拷贝的说法。
二、浅拷贝
浅拷贝是只复制了一层的数据,对于更深层次的数据,还是址传递。
let obj1 = {
a: 10,
b: {
m: 20
}
}
let obj2 = shallowCopy(obj1); // 假设的浅拷贝函数
假设我们对上面的obj1对象,使用了某种浅拷贝方法,得到了obj2,那么它们在内存中的关系就是下图这种:
1. Object.assign()
Object.assign()会把一个对象的可枚举属性逐一复制一份给另一个对象,但是如果对象的属性值是引用类型的话,那也只是复制引用地址:
let obj1 = {
a: 10,
b: {
m: 20
}
}
let obj2 = Object.assign({}, obj1);
console.log(obj2.a); // 10
console.log(obj2.b.m); // 20
obj1.a = 100;
obj1.b.m = 200;
console.log(obj2.a); // 10
console.log(obj2.b.m); // 200
可以看出在使用Object.assign()浅拷贝之后,属性a是基础类型,所以修改obj1.a不会影响obj2.a,而b属性是引用类型,修改obj1.b.m的值会影响obj2.b.m。
2. 扩展运算符
let obj1 = {
a: 10,
b: {
m: 20
}
}
let obj2 = {...obj1}
console.log(obj2.a); // 10
console.log(obj2.b.m); // 20
obj1.a = 100;
obj1.b.m = 200;
console.log(obj2.a); // 10
console.log(obj2.b.m); // 200
es6的…扩展运算符用在对象上也是浅拷贝,扩展运算符也可以用在数组上。
3. for…in
使用for…in实现简单的浅拷贝:
function shallowCopy(obj) {
let result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
result[key] = obj[key];
}
return result;
}
let obj1 = {
a: 10,
b: {
m: 20
}
}
let obj2 = shallowCopy(obj1);
console.log(obj2.a); // 10
console.log(obj2.b.m); // 20
obj1.a = 100;
obj1.b.m = 200;
console.log(obj2.a); // 10
console.log(obj2.b.m); // 200
for…in只遍历了第一层的属性,所以这里也是浅拷贝,这个函数对数组也是浅拷贝,这里就不演示了。
4. slice()和concat()
slice()和concat()属于数组的方法,对数组也可以实现浅拷贝:
let arr1 = [1, 2, [3, 4]]
let arr2 = arr1.slice();
let arr3 = arr1.concat();
console.log(arr2); // [1, 2, [3, 4]]
console.log(arr3); // [1, 2, [3, 4]]
arr1[0] = 10;
arr1[2][0] = 30;
console.log(arr2); // [1, 2, [30, 4]]
console.log(arr3); // [1, 2, [30, 4]]
三、深拷贝
了解了浅拷贝的效果,现在大概也能猜到深拷贝是什么样了,其实就是所有对所有层次的属性都进行复制一份备份数据,互相独立,任何层次属性的修改都不会影响另一个对象。
let obj1 = {
a: 10,
b: {
m: 20
}
}
let obj2 = "深拷贝"(obj1)
同样假设对上面这个对象,使用某种深拷贝的方法,得到obj2,那么它们在内存中的关系就如图下所示:
1. JSON字符串
JSON.stringify()是将传入的数据转为JSON字符串的方法,JSON.parse()是将JSON字符串转为js数据的方法,通过这两种方法的组合,就能实现深拷贝的效果了:
let obj1 = {
a: 10,
b: {
m: 20
}
}
let arr1 = [1, 2, [3, 4]];
let obj2 = JSON.parse(JSON.stringify(obj1));
let arr2 = JSON.parse(JSON.stringify(arr1));
obj1.a = 100;
obj1.b.m = 200;
arr1[0] = 10;
arr1[2][0] = 30;
console.log(obj2.a); // 10
console.log(obj2.b.m); // 20
console.log(arr2); // [1, 2, [3, 4]]
这个方法有一个问题,就是不能复制函数类型的数据。
2. 递归函数
在上面我们看到浅拷贝都是只拷贝第一层属性值或数组值,如果值是引用地址,那也只是复制地址,不会深入内部复制数据。要手动实现深拷贝,一个思路就是探索对象或数组的每一层,对每一层都是用浅拷贝,这样,就可以达到深拷贝的效果。
下面是一种简单的深拷贝实现:
function deepCopy(data) {
// 判断拷贝的是对象还是数组,初始化一个变量保存结果
let result = Array.isArray(data) ? [] : {};
// 如果是对象或者数组类型,就遍历厘里面的属性或元素
if (data !== null && typeof data === 'object') {
// 判断是数组还是
if (Array.isArray(data)) {
data.forEach(index => {
result[index] = deepCopy(data[index])
})
}else {
Object.keys(data).forEach(key => {
result[key] = deepCopy(data[key]);
})
}
}else {
// 如果是基本类型或者函数类型的数据,则直接把data保存在result里
result = data;
}
// 返回结果
return result;
}
let obj1 = {
a: 10,
b: {
m: {
n: 20
}
},
c: function () {
console.log('before');
}
}
let obj2 = deepCopy(obj1);
obj1.a = 100;
obj1.b.m.n = 200;
obj1.c = function () {
console.log("after");
}
console.log(obj2.a); // 10
console.log(obj2.b.m.n); // 20
obj2.c(); // before
使用递归函数进行深拷贝之后,不仅属性可以被拷贝,方法也能被拷贝,修改属性和方法都不影响另一个,这个函数对数组也有效,这里就不再演示。
3. $.extend()
这个方法时jQuery的方法,也能实现深拷贝:
let obj1 = {
a: 10,
b: {
m: {
n: 20
}
},
c: function () {
console.log('before');
}
}
let obj2 = $.extend(true, {}, obj1)
obj1.a = 100;
obj1.b.m.n = 200;
obj1.c = function () {
console.log("after");
}
console.log(obj2.a); // 10
console.log(obj2.b.m.n); // 20
obj2.c(); // before
$.extend()当做深拷贝使用时有三个参数,第一个判断是否要深拷贝,第二个参数是目标对象,第三个是源对象。