类型和堆栈#
在讲深浅拷贝前,有必要讲下 JavaScript 的内置类型。JavaScript 有 2 种类型,基本类型和引用类型。
基本类型包括string、number、boolean、null、undefined、symbol (ES6 新增类型)6 种,以栈的形式存储在内存中。
引用类型主要是 object 类型,包括Object、Array、Date、RegExp等多种类型,以堆的形式存储在内存中。
一般我们声明一个变量,如果变量是基本类型,则变量的值存储在栈内存中。而如果变量是引用类型,变量的值会指向堆内存中引用类型的地址。
赋值、浅拷贝和深拷贝的区别#
将某一数值赋给某个变量的过程,称为赋值。基本类型对象赋值是直接在栈内存中创建一个值来。而引用对象的赋值就不一样了,而是将原对象或原数组的引用直接赋给新对象/新数组,新对象/数组只是原对象的一个引用,
当原来的对象或者数组的值改变时,新对象或者新数组也同样跟着改变,同理新对象或者新数字的值改变时,原来的对象或者数组的值也会改变,因为它们的值都来同一个引用,即堆内存中的某个地址的值。
let a = 1;
let b = a;
a = 2;
console.log(b); // 1 赋值后基本对象的值不会随着原来的值的改变而改变
const obj1 = { a: 1, b: 3 };
const obj2 = obj1;
obj1.a = 2;
console.log(obj2.a); // 2 赋值后引用类型的值随着原对象的改变而改变
obj2.b = 4;
console.log(obj1.b); // 4 同理,原对象值也会随着赋值后对象的值的改变而改变如果我们在处理引用类型数据到时候,为了防止影响到原来的值,这个时候就不能使用赋值,而是应该使用拷贝。
通过拷贝,在堆内存中重新创建一个新的地址,使得拷贝后变量的值指向新的地址的值,这个地址的值和原来的值是一样的,但是它们是独立的,不会被互相影响。
拷贝有浅拷贝和深拷贝。
- 浅拷贝:创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”,只拷贝第一层。
- 深拷贝:创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”,拷贝所有层。
浅拷贝#
浅拷贝只拷贝原对象的第一层,而再深一层的对象或者数组还是原对象的应用。浅拷贝实现非常多,最简单的方法是使用 JS 自带的 API 来可以实现。
对象的浅拷贝
const obj = {
a: 1,
};
const cloneObj = Object.assign({}, obj); // Object.assign API
const cloneObj = { ...obj }; // 解构赋值数组的浅拷贝
const arr = [1, 2, 3];
const cloneArr = Array.from(arr); // Array.from API
const cloneArr = arr.slice(); // 切片
const cloneArr = [...arr]; // 解构赋值深拷贝#
上面的浅拷贝,只能拷贝了引用类型的第一层的值。如果在更深一层中还存在引用类型,假设这个是为c,则c的值改指向原来的引用类型的值。
const obj = {
a: 1,
b: {
c: 3,
},
};
const cloneObj = Object.assign({}, obj);
console.log(cloneObj); // 与 obj 一样
obj.a = 10; // 修改第一次的基本类型的值
obj.b.c = 33; // 修改更深一层的引用类型的值
console.log(cloneObj.a); // 1 基本类型的值不变
console.log(cloneObj.b); // { c: 33 } // cloneObj 的值也跟着变了从上面可以看出,浅拷贝之后的新值如果原来的值修改了更深一层的值后,它的值也会跟着改变,因为浅拷贝只拷贝了第一层,更深一层的值还是原来值的引用,原来的值会影响到浅拷贝的新值。
再看一个示例:
const obj = {
a: 1,
b: {
c: 3,
},
};
const cloneObj = Object.assign({}, obj);
obj.b = { d: 4 }; // 修改第一层引用类型的值
console.log(cloneObj.b); // { c: 3 } 不变为什么这里修改了原对象 b 的值,而浅拷贝的值并没有变化呢?
因为原对象 obj 中 b 的值是引用类型,它指向堆内存中一个地址,它的值为{ c: 3 }的地址。再修改了 b 的值后,相当于把 b 的值指向了堆内存中另一个地址,它的值{d: 4}。
通过浅拷贝,堆内存中生产了一个新的地址,它的值也是{ c: 3 },他是独立的,而新的对象 cloneObj 中 b 就是指向这个地址。所以上面代码中,修改了obj.b的值,对cloneObj.b的值一点都没有影响。
但是为什么修改了obj.b.c = 33后,就会对浅拷贝后的对象的值有影响呢?
那是因为浅拷贝的值只拷贝了第一层,虽然它的值指向了一个新的内存地址的值为{ c: 3 }, 但是这个内存地址里面 c 的值3和 原对象 b 下面 c 的值3指向的是同一个内存地址,所有它们都是同一个内存地址值的引用,修改了原对象的值之后,浅拷贝的值也会跟着一起修改。

如果需要使得拷贝后的对象中 c 的值是不被原来的对象所影响,就需要使用深拷贝了,在堆内存中创建新的值。
使用深拷贝,可以自己封装,也可以直接使用第三方库。比如借助非常有名的第三方工具库lodash的实现深拷贝。
const { cloneDeep } = require("lodash");
const obj = {
a: 1,
b: {
c: 3,
},
};
const cloneObj = cloneDeep(obj);lodash的cloneDeep已经非常完美的处理了比如Date、RegExp等多种引用类型的值,而且经过了测试,推荐使用。
下面手动实现深拷贝函数。
手动实现深拷贝#
json 转换实现
使用 JSON 的 API 实现深拷贝函数是最简单的,通过 JSON 的 2 次转换即可,代码如下:
function deepClone1(obj) {
return JSON.parse(JSON.stringify(obj));
}递归
通过判断拷贝后的值是否是引用类型时,进行递归,直到所有的值都是基本类型为止,这样也可以实现深拷贝函数。
当然,其他还需要处理一下其他的类型比如Array``Date 等类型的值,另外也可以加上缓存,提高性能。
示例代码 1:
function deepClone2(src) {
let clone = Object.assign({}, src); // 浅拷贝
// 遍历递归
Object.keys(clone).forEach(
(key) =>
(clone[key] =
typeof src[key] === "object" ? deepClone(src[key]) : src[key])
);
// 返回值
return Array.isArray(src) && src.length // 非空数组
? (clone.length = src.length) && Array.from(clone) // 设置 length 属性,转换成数组
: Array.isArray(src) // 空数组
? Array.from(src)
: clone; // 对象
}示例代码 2:
function deepClone(obj, hash = new WeakMap()) {
if (obj instanceof RegExp) return new RegExp(obj); // 处理 RegExp 类型的值
if (obj instanceof Date) return new Date(obj); // 处理 Date 类型的值
if (obj === null || typeof obj !== "object") {
return obj;
}
if (hash.has(obj)) {
return hash.get(obj);
}
let t = new obj.constructor();
hash.set(obj, t);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
t[key] = deepClone(obj[key], hash);
}
}
return t;
}总结#
实际上,浅拷贝已经可以解决大部分使用的场景需求了。如果需要用到深拷贝,大多数场景中只需要处理下对象和数组类型即可。如果需要处理复杂引用类型的数据的拷贝,强烈推荐使用lodash库。