Skip to main content

JavaScript知识之 浅拷贝与深拷贝

· 6 min read
kart jim

值传递与引用传递

JavaScript中有两种数据类型:基本数据类型引用数据类型两种。
从名字上来看,大概也能猜到它们的区别:

  • 基本数据类型
    • 值直接存储在栈内存中
  • 对于引用类型来说
    • 它存储了一个引用,而真正的数据存储在堆内存中

当基本数据类型 a 赋值给另一个基本数据类型 b 时,是值传递;当 a 值变化后, b 的值并不会跟着变化:

let a = 5;
b = a; // a 赋值给b
console.log(a); // 5
console.log(b); // 5
a = 15; // a变化
console.log(a); // 15
console.log(b); // 5

base1 在这里插入图片描述

而当引用数据类型赋值给另一个基本数据类型时,是引用传递;当 a 值变化后, b 的值会跟着变化:

let a = [1,2,3,4];
b = a; // a 赋值给b
console.log(a); // [ 1, 2, 3, 4 ]
console.log(b); // [ 1, 2, 3, 4 ]
a[1] = 15;
console.log(a); // [ 1, 15, 3, 4 ]
console.log(b); // [ 1, 15, 3, 4 ] b跟着变化

之所以会跟着变化,是因为引用数据类型存储的是一个引用,当它赋值给另一个引用数据时,只会把引用赋值给它;所以当原来的引用对应的值变化了,由于两个引用指向的是一个值,所以也会随着变化。

在这里插入图片描述 => 在这里插入图片描述

浅拷贝与深拷贝

上面我们知道了引用数据类型赋值给另一个引用数据类型时,两个变量指向同一个数组,这就是所谓浅拷贝。

浅拷贝==只拷贝对象的顶级属性==,嵌套的引用类型则直接复制引用。

但是,有的时候,我们需要将一个变量中的 所有值都拷贝一份,当被赋值的变量变化时不会影响到原来的变量;这时候就需要深拷贝:

对于有嵌套的引用类型(如:==js对象==、==数组==、==Map==、==Set==、==RegExp==等),将一个变量中的 所有值都拷贝一份,使变化时不影响原先变量,就是深拷贝。

深拷贝有很多种方法,可以直接撸 js 代码,通过递归实现;也可以通过第三方库实现;甚至可以利用 JSON 里的方法实现;
下面介绍:

利用 JSON 方法

这种方法只适用于 对 js对象 实现深拷贝!

JSON.stringify()JSON.parse(),可以将原来的数据先变化为JSON字符串,然后再还原为js对象。

let obj = {
a: [1,3,5,6,[6,7]],
b: new Set([2,3,4]),
c: /string/g
}
let objString = JSON.stringify(obj);
let obj2 = JSON.parse(objString);
let obj3 = obj; // 浅拷贝

console.log(obj); // { a: [ 1, 3, 5, 6, [ 6, 7 ] ], b: Set(3) { 2, 3, 4 }, c: /string/g }
console.log(obj2); // { a: [ 1, 3, 5, 6, [ 6, 7 ] ], b: {}, c: {} } 数据丢失
console.log(obj === obj2); // false
console.log(obj === obj3); // true

可以看到,这种方法实现深拷贝可能会出现数据丢失情况;非常不建议使用

递归函数实现

这种方法的想法很简单,就是利用递归调用,分类讨论不同的数据类型,然后寻找变量最底层的 基本数据结构,利用值传递赋值,开辟新的引用。
简单实现:

// 简单实现
function deepClone(obj){
// 基本数据类型本身就是值传递
if(obj === null || typeof obj !== 'object'){
return obj;
}
if(obj instanceof Set){ // Set 类型
const t = new Set();
obj.forEach((val)=>{
t.add(deepClone(val));
})
return t;
} else if(obj instanceof Map){ // Map 类型
const t = new Map();
obj.forEach((val,i)=>{
t.set(i,deepClone(val));
})
return t;
} else if(obj instanceof RegExp){ // 正则 类型
return new RegExp(obj);
} else { // 其它 js对象
const t = new obj.constructor();
for(const i in obj){
t[i] = deepClone(obj[i]);
}
return t;
}
}

测试:

// test code
const obj = {
a: 1,
b: new Set([1,2,3,4,new Set([5,6])]),
c: new Map([
['a','Number'],
['b','Set'],
['c','Map'],
['d','RegExp'],
['e','Object']
]),
d: /^regexp/g,
e: [{a:10},new Date()]
}
const deep = deepClone(obj); // 深拷贝
const shadow = obj; // 浅拷贝

console.log(obj === deep); // false
console.log(obj === shadow); // true

console.log(obj.a === deep.a); // true
console.log(obj.a === shadow.a); // true
console.log(obj.b === deep.b); // false
console.log(obj.b[4] === deep.b[4]); // true
console.log(obj.b === shadow.b); // true

第三方库

第三方库都有封装好的方法,可以直接使用:

  • Lodash
    • _.cloneDeep(value)方法
  • Ramda
    • R.clone(objects)方法

有兴趣的可以去 github 阅读这些第三方库的底层实现原理: