本文解析 JavaScript 中深拷贝与浅拷贝的核心区别,阐述基本类型与引用类型的内存存储机制。详细介绍 JSON.parse/stringify、递归手写及 jQuery.extend 三种实现深拷贝的方法,并指出 slice 和 concat 无法实现真正深拷贝的误区。
一、自我理解
简单来说,深拷贝会递归复制所有层级的数据,而浅拷贝仅复制最外层的数据结构。
在深拷贝场景下,修改新对象不会波及原对象;而在浅拷贝中,对新对象内部引用类型的修改会同步反映到原对象上。
深拷贝生成的新对象与原对象拥有完全独立的属性存储空间,而浅拷贝生成的新对象与原对象共享部分属性引用。
举例说明:假设有数据 A 和 B,若 B 是 A 的拷贝,当修改 A 时,如果 B 随之变化,则为浅拷贝;若 B 保持不变,则为深拷贝。
let A = [0,1,2]
let B = A
console.log(A === B);
A[0] = 3
console.log(A,B);结果如下:

这是一个典型的浅拷贝案例。尽管 B 复制了 A,但修改 A 后,B 的值也发生了同步变化。
二、数据存储形式
通过上述案例,我们需要理解数据类型的存储机制。若对数据类型尚不熟悉,可参考《JS中8种数据类型》一文。
- 基本数据类型包括:number、string、boolean、null、undefined、symbol(ES6 新增),以及 Chrome 67 版本引入的 BigInt(任意精度整数)。
- 引用数据类型主要为 Object,这是一个大类,涵盖 function 函数、array 数组、date 日期等子类型。
(1)基本数据类型存储于栈中
变量名及其对应的值均存储在栈内存中。
每次声明变量时,系统会开辟新的内存空间来存储值,例如执行 let a = 1。
// 声明一个变量
let a = 1;
当 b 复制 a 时,由于 b 也是变量,栈内存会再次开辟新的空间来存储 b 的值。
// 声明一个变量
let a = 1;
let b = a; //b变量复制a变量
console.log(a,b); //1 1
由此可见,a 和 b 拥有独立的内存空间,因此修改其中一个变量的值不会影响另一个变量。
// 声明一个变量
let a = 1;
let b = a; //b变量复制a变量
console.log(a,b); //1 1
// 修改a的值不影响b
a = 123;
console.log(a,b); //123 1(2)引用数据类型存储于堆中
变量名存储在栈中,而实际值存储在堆中,栈内存中保存一个指向堆内存的指针。
声明引用类型时,栈内存中会生成一个指针指向堆内存中的具体值。
// 声明一个引用类型的值
let a = [0,1,2,3,4]当 b 复制 a 时,仅复制了栈内存中的指针,两者指向同一个堆内存地址,并未复制堆中的实际值。
let a = [0,1,2,3,4]
let b = a //复制的是一个指针,而不是堆中的值
因此,执行 a[0]=1 时,指针指向的堆内存值发生改变。由于 a 和 b 指向同一地址,b 的值也会随之改变,这就是浅拷贝的行为。
// 声明一个引用类型的值
let a = [0,1,2,3,4];
let b = a; //复制的是一个指针,而不是堆中的值
console.log(a === b);
a[0] = 1; //改变a数组值,b数组跟着改变(浅拷贝)
console.log(a,b);
要实现深拷贝,需在堆内存中开辟新空间专门存储 b 的值,使其与基本数据类型的处理逻辑一致,从而确保修改一个值不影响另一个值。

三、怎样实现深拷贝?
(1)借助 JSON 对象的 parse 和 stringify
parse() 方法用于将 JSON 字符串解析为 JavaScript 对象。
stringify() 方法用于将 JavaScript 对象转换为 JSON 字符串。
// 浅拷贝
let arr = [1,"hello",{name:"张三",age:21}]
let copyArr = arr
console.log(arr === copyArr); //true
arr[1] = "您好"
console.log("浅拷贝",arr, copyArr);
// JSON对象方法实现深拷贝
let arr = [1,"hello",{name:"张三",age:21}]
let newArr = JSON.parse(JSON.stringify(arr)) //将arr数组转换为字符串,再作为参数转化为对象
arr[1] = "您好"
arr[2].name = "法外狂徒"
console.log("深拷贝",arr, newArr);
其原理是利用 JSON.stringify 将对象序列化为字符串,再通过 JSON.parse 反序列化为新对象,从而在堆内存中开辟新空间,实现深拷贝。
这种写法简洁且适用于大多数场景,但存在明显缺陷,例如无法正确处理其他引用类型、函数以及循环引用等情况。
(2)手写递归
递归实现深度克隆的原理是:遍历对象和数组,直到内部元素均为基本数据类型,然后进行复制,即深度拷贝。
- 若遇到原始类型,无需继续递归,直接返回该值即可。
- 若遇到引用类型,则创建新对象,遍历待克隆对象的属性,将执行深拷贝后的属性依次添加到新对象上。
// 手写递归实现深拷贝
function clone(target) {
if (typeof target === 'object') {
// 创建一个新对象
let cloneTarget = {};
// 遍历需要克隆的对象
for (const key in target) {
// 将需要克隆对象的属性执行深拷贝后依次添加到新对象上
cloneTarget[key] = clone(target[key]);
}
// 返回克隆对象
return cloneTarget;
} else {
return target;
}
};
// 模拟拷贝对象
const person = {
name: "李小白",
age: 18,
job: {
name:"web前端",
salary:"15k",
address:"成都"
}
};
// 将需要克隆的对象传递给clone函数作为参数,接收一个克隆对象作为返回结果
let newTarget = clone(person)
//修改原对象属性
person["name"] = "李大白"
person["job"]["salary"] = "25k"
console.log(person,newTarget); //实现深拷贝
利用递归可实现深度拷贝,但存在局限性,例如未处理数组类型。
要兼容数组,只需判断参数是否为数组,利用三元运算符决定创建新对象还是新数组即可。
function clone(target) {
if (typeof target === 'object') {
let isArr = Array.isArray(target)
// 判断传入的参数是数组吗 ? 是则赋值[] : 否则赋值{}
let cloneTarget = isArr ? [] : {};
// 遍历需要克隆的对象
for (const key in target) {
// 将需要克隆对象的属性执行深拷贝后依次添加到新对象上
cloneTarget[key] = clone(target[key]);
}
// 返回克隆对象
return cloneTarget;
} else {
return target;
}
};(3)JQuery 中 extend 方法
jQuery 库提供了 $.extend() 方法来实现深拷贝。
语法格式如下:
$.extend( [deep ], target, object1 [, objectN ] )- deep 参数表示是否进行深拷贝,设为 true 表示深拷贝,设为 false 则为浅拷贝。
- target 为 Object 类型,即目标对象,其他对象的成员属性将被附加到该对象上。
- object1 至 objectN 为可选参数,Object 类型,表示第一个以及第 N 个被合并的对象。
let arr = ["李小白",18,["前端","15k"],21]
// true:开启深拷贝,目标对象:[],原对象:arr
let cloneArr = $.extend(true, [], arr)
arr[0] = "李大白";
arr[2][0] = "java";
console.log(arr, cloneArr);
当 $.extend() 的第一个参数为 true 时可实现深拷贝;若为 false,则仅拷贝第一层属性,无法实现深拷贝。
let arr = ["李小白",18,["前端","15k"],21]
// false:不开启深拷贝,目标对象:[],原对象:arr
let cloneArr = $.extend(false, [], arr)
arr[0] = "李大白";
arr[2][0] = "java";
console.log(arr, cloneArr);
总结/注意
实现深拷贝的方式有多种,上述三种较为常用。此外,函数库 lodash 中的 _.cloneDeep 方法(本人未使用过)也可实现深拷贝。
最后需要补充说明,网上常有人声称 slice 和 concat 方法可实现深拷贝,但事实上,slice 和 concat 方法并不能实现真正的深拷贝。
下面举例说明:
// slice()
let arr = [0,1,["hello","world"]];
let cloneArr = arr.slice();
arr[0] = "零";
arr[2][0] = "hello2022"
console.log(arr,cloneArr);// concat()
let arr = [0,1,["hello","world"]];
let cloneArr = arr.concat();
arr[0] = "零";
arr[2][0] = "hello2022"
console.log(arr,cloneArr);这两个方法得到的结果表现一致:

这一点务必注意,深拷贝必须拷贝所有层级的属性,而这两个方法拷贝不彻底,仅能处理第一层,希望大家避免踩坑。
总结
到此这篇关于 js 中区分深拷贝与浅拷贝的文章就介绍到这了,更多相关 js 区分深拷贝与浅拷贝内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!