本文深入解析 JavaScript 中浅拷贝与深拷贝的核心区别及多种实现方式。通过 Object.assign、扩展运算符、JSON 序列化及递归手写等代码示例,详细对比各方法的优缺点与局限性,帮助开发者根据实际场景选择正确的对象复制策略。
浅拷贝
浅拷贝会创建一个新的对象,用于接收原对象的值。若属性为基本数据类型,新对象将获得该值的副本;若属性为引用数据类型,新对象仅保存指向原内存地址的引用。因此,修改新对象中的引用类型属性,会同步影响原对象。

实现方法
方法一:Object.assign
这是 ES6 中 Object 对象提供的一个方法,主要用于对象的合并,并返回目标对象。需要注意的是,该方法不会拷贝对象的继承属性以及不可枚举的属性。
let target = {};
let source = {a:{b:1}};
Object.assign(target,source)
console.log(taget) // {a:{b:1}}
target.a.b = 2;
console.log(taget) // {a:{b:2}}
console.log(source) // {a:{b:2}}方法二:扩展运算符方式
/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj) //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj) //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; 方法三:concat和slice 浅拷贝数组
此方法仅适用于数组类型,执行后会返回一个新的数组对象。
concat 浅拷贝数组
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr); // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]
slice 浅拷贝数组
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr); //[ 1, 2, { val: 1000 } ]深拷贝
深拷贝会将对象及其所有嵌套属性从内存中完整复制一份到目标对象,并在堆内存中开辟全新的空间存放新对象。修改新对象不会改变原对象,两者实现真正的相互独立。

实现方法
方法一:乞丐版(JSON.stringify和JSON.parse)
let obj1 = { a:1, b:[1,2,3] }
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log(obj2); //{a:1,b:[1,2,3]}
obj1.a = 2;
obj1.b.push(4);
console.log(obj1); //{a:2,b:[1,2,3,4]}
console.log(obj2); //{a:1,b:[1,2,3]}缺陷:
- 若对象值中包含函数、undefined 或 symbol 类型,经 JSON.stringify 序列化后,这些键值对将从结果字符串中消失。
- Date 引用类型在序列化后会转换为字符串。
- 无法拷贝不可枚举的属性。
- 无法拷贝对象的原型链。
- RegExp 引用类型在序列化后会变为空对象。
- 若对象包含 NaN、Infinity 或 -Infinity,JSON 序列化后的结果将变为 null。
手写递归实现
基础版
let obj = {a:{b:1}};
function deepClone(obj){
let cloneObj = Object.prototype.toString.call(obj) === "[object Array]" ? [] : {};
for(let key in obj){
if(typeof obj[key]=== "object" && obj !== null){
cloneObj[key] = deepClone(obj[key])
}else{
cloneObj[key] = obj[key]
}
}
return cloneObj;
}
let obj2 = deepClone(obj);
obj2.a.b = 33
console.log("obj",obj) // {a:{b:1}}
console.log("obj2",obj2) // {a:{b:33}}缺陷:
- 无法复制不可枚举的属性以及 Symbol 类型。
- 该方式仅针对普通引用类型进行递归复制,对于 Array、Date、RegExp、Error、Function 等特殊引用类型无法正确拷贝。
- 未解决对象属性成环(循环引用)的问题。
改进版
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date)
return new Date(obj) // 日期对象直接返回一个新的日期对象
if (obj.constructor === RegExp)
return new RegExp(obj) //正则对象直接返回一个新的正则对象
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) return hash.get(obj)
let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
//继承原型链
hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}手写深拷贝参考:
https://www.jb51.net/article/247540.htm
https://www.jb51.net/article/247544.htm
其他实现方法
可使用 lodash 库的 _.cloneDeep 方法,或 jQuery.extend() 方法。
FAQ:赋值和深浅拷贝的区别
注意:以下讨论的前提均针对引用类型。
赋值
当将一个对象赋值给新变量时,实际赋的是该对象在栈内存中的地址,而非堆中的数据。这意味着两个变量指向同一存储空间,无论修改哪个对象,改变的都是该存储空间的内容,因此两个对象是联动的。
let a = {name:'jimmy',b:{age:12}}
let b = a;
b.name = 'chimmy';
b.b.age = 21;
console.log(a) // {name:'chimmy',b:{age:21}}
console.log(b) // {name:'chimmy',b:{age:21}}浅拷贝
浅拷贝会在堆中重新创建内存,拷贝前后的基本数据类型互不影响;但引用类型因共享同一块内存,修改其中一个会影响另一个。
let a = {name:'jimmy',b:{age:12}}
let b = {...a};
b.name = 'chimmy';
b.b.age = 21;
console.log(a) // {name:'jimmy',b:{age:21}}
console.log(b) // {name:'chimmy',b:{age:21}}深拷贝
深拷贝将对象从内存中完整拷贝一份给目标对象,并在堆内存中开辟全新空间存放新对象。修改新对象不会改变原对象,二者实现真正的分离。
let a = {name:'jimmy',b:{age:12}}
let b = {...a};
b.name = 'chimmy';
b.b.age = 21;
console.log(a) // {name:'jimmy',b:{age:12}}
console.log(b) // {name:'chimmy',b:{age:21}}总结
到此这篇关于js深拷贝和浅拷贝的文章就介绍到这了,更多相关js深拷贝和浅拷贝内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!