浅拷贝与深拷贝
1.赋值
将某个对象赋值给一个变量,这个变量仅仅是拿到了该对象的引用,也即拿到了对象在栈中的内存地址,而不是对象存储在堆中的数据。因此,无论对象中的属性是基本数据类型还是引用数据类型,修改了这个变量的属性,所对应的对象的属性也会更改。
$(function (){ let o = { name:"Tom", age:16, friends:["Mike","John","Tim"] } let p = o; p.age = 18; p.friends.push("Mary"); console.log(p); console.log(o); })
控制台输出如下:
可以看到,修改变量p会导致原对象o的属性也发生改变。
2.浅拷贝
相比于直接赋值,浅拷贝会创建新的对象,然后对原对象按位拷贝——对于值为基本数据类型的属性,它会复制一份新的基本数据类型的值给新对象的对应属性;但对于值为引用数据类型的属性,新对象拷贝的只是它的内存地址,也就是说,新对象的引用数据类型属性若修改,原始对象也会随着更改。
浅拷贝的实现方法
1.手写浅拷贝
$(function (){ let o = { name:"Tom", age:16, friends:["Mike","John","Tim"] } function shallowcopy(source){ let obj = {}; for(let prop in source){ obj[prop] = source[prop]; } return obj; } let p = shallowcopy(o); p.age = 18; p.friends.push("Mary"); console.log(p); console.log(o); })
控制台输出如下:
可以看到,对于值为基本数据类型的"age"属性,修改p.age的值不影响对象o的age属性,但修改值为引用数据类型的属性friends时,对原始对象也进行了修改,因为p拿到的是friends属性在内存中的地址,修改的时候是修改该地址的内容,因此会影响原始对象。
2.Object.assign(target, ...source)
该方法可以将所有可枚举属性的值从一个或多个源对象分配到目标对象,返回目标对象。对于继承属性与不可枚举属性是不能拷贝的。
$(function (){ let o = { name:"Tom", age:16, friends:["Mike","John","Tim"] } let o2 = Object.create({foo:0}); Object.defineProperty(o2,"un",{value:1,enumerable:false}); Object.defineProperty(o2,"en",{value:2,enumerable:true}); let p =Object.assign({},o,o2); p.age = 18; p.friends.push("Mary"); console.log(p); console.log(o); })
控制台输出如下:
这里调用了Object.create()来创建对象o2,对象{foo:0}作为Object.create()的参数,是充当新创建对象o2的原型的,因此o2会继承对象{foo:0}的"foo"属性。接下来,使用Object.defineProperty来为对象o2定义新的属性"un"(不可枚举)和"en"(可枚举)。可以看到,对于继承属性"foo"和不可枚举属性"un"都没有拷贝到对象p中。
3.利用数组方法
Array.prototype.concat()、Array.prototype.slice()或者 [...arr] 这些方法都不会在原数组上修改,而是返回新的数组,因此可以实现数组的浅拷贝。
$(function (){ let arr = [1,2,3,{foo:1}]; let p1 = arr.concat(); let p2 = arr.slice(); let p3 = [...arr]; p1[0] = "p1"; p2[0] = "p2"; p3[0] = "p3"; p3[3].foo =2; console.log(arr); console.log(p1); console.log(p2); console.log(p3); })
控制台输出如下:
可以看到,对于基本数据类型,每个变量都修改了各自的副本,但对于引用数据类型,它们拿到的只是一个内存地址,所有任意一个变量修改了引用数据类型,其他变量也跟着改变。
4.$.extend()方法
该方法既可以实现浅拷贝,也可以实现深拷贝。当传入两个参数时,$.extend(target,source)实现的是浅拷贝。
2.深拷贝
相比于浅拷贝,深拷贝创建的对象不会与原始对象共享内存,因此对应值为引用数据类型的属性,拷贝的是内容并将内容放在一块新的内存上,作为新对象的属性,而不像之前那样拷贝地址。
深拷贝的实现方法
1.手写递归实现
不考虑继承与循环引用问题,代码如下:
$(function (){ function deepclone(source){ if(source == null) return null; let obj = source instanceof Array?[]:{}; for(let prop in source){ if(source.hasOwnProperty(prop)){ obj[prop] = typeof source[prop] == 'object'?deepclone(source[prop]):source[prop]; } } return obj; } let o = { x:1, y:{ o:null, s:[undefined,{foo:"a"}] }, z:[1,2,"a"], fn:function (){ console.log("O"); } } let p = deepclone(o); p.fn = function (){ console.log("P"); } p.z.push(3); console.log(p); console.log(o); p.fn(); o.fn(); })
控制台输出如下:
2.JSON.parse()与JSON.stringify()
利用JSON.stringify()将JavaScript值或对象转换为JSON字符串,再通过JSON.parse()将这个JSON字符串解析为JavaScript值或对象,从而实现深拷贝。但只能拷贝JSON语法支持的值,也即:数值Number、布尔值Boolean、字符串String、null、数组Array和对象,而无法拷贝值为undefined,Function的属性。
$(function (){ let o = { num:123, other:{ a:[1,2,{foo:0}] }, un:null, nn:undefined, fn:function (){ } } let temp = JSON.stringify(o); let p = JSON.parse(temp); p.num = 234; p.other.a[2].foo = 9; console.log(p); console.log(o); })
控制台输出如下:
可以看到,p并没有将对象o中值为undefined的"nn"属性以及值为函数的"fn"属性拷贝成功。
3.$.extend()方法
当传入$,extend(true,target,source)实现的就是深拷贝。
$(function (){ let o = { num:123, other:{ a:[1,2,{foo:0}] }, un:null, nn:undefined, fn:function (){ } } let p = $.extend(true,{},o); p.other.a.push(3); console.log(p); console.log(o); })
控制台输出如下:
该方法有两个弊端:
- 无法拷贝值为undefined的属性;
- 没有解决循环引用的问题(如下面代码);
$(function (){ let a = {}; let b = {}; a.x = b; b.y = a; let p = $.extend(true,{},a); })
输出报错:
一个解决了循环引用问题的综合方法(浅+深拷贝)
let clone = (function () { let classof = function (o) { //使用Object.prototype.toString()来检测数据类型,因为返回值为[object type]因此从第8位来末尾就是标识类型的字符串 let className = Object.prototype.toString.call(o).slice(8, -1); return className; } //存储保存过的属性,每次遍历属性时都查询reference数组,防止属性循环引用 let reference = null; //遇到不同类型的对象的处理方式 let handlers = { //正则表达式 'RegExp': function (reg) { let flags = ''; flags += reg.global ? 'g' : ''; flags += reg.multiline ? 'm' : ''; flags += reg.ignoreCase ? 'i' : ''; return new RegExp(reg.source, flags); }, //时间对象 'Date': function (date) { return new Date(+date); }, //数组 'Array': function (arr, shallow) { let newArr = []; for (let i = 0; i < arr.length; i++) { if (shallow) { newArr[i] = arr[i]; } else { //对于深拷贝,每次拷贝前要查询是否已经拷贝过该属性,防止某些属性循环引用 if (reference.indexOf(arr[i]) !== -1) { continue; } //同样,对于数组中元素是基本数据类型,handlers返回的是undefined,直接跳到else子句里 let handler = handlers[classof(arr[i])]; if (handler) { reference.push(arr[i]); newArr[i] = handler(arr[i], false); } else { newArr[i] = arr[i]; } } } return newArr; }, 'Object': function (obj, shallow) { let newObj = {}; for (let prop in obj) { if (obj.hasOwnProperty(prop)) { if (shallow) { newObj[prop] = obj[prop]; } else { if (reference.indexOf(obj[prop]) !== -1) { continue; } //同样,对于对象中属性值是基本数据类型,handlers返回的是undefined,直接跳到else子句里 let handler = handlers[classof(obj[prop])]; if (handler) { reference.push(obj[prop]); newObj[prop] = handler(obj[prop], false); } else { newObj[prop] = obj[prop]; } } } } return newObj; } }; return function (obj, shallow) { reference = []; //若没有传递shallow参数,默认为浅拷贝 shallow = shallow === undefined ? true : false; //对于基本数据类型,handlers对象中是没有对应的属性的,因此会返回undefined。若是undefined,后面语句会直接return obj。 let handler = handlers[classof(obj)]; return handler ? handler(obj, shallow) : obj; }; }());
let a = {z1:1};
let b = {z2:2};
a.x = b;
b.y = a;
let p = clone(a,false);
console.log(p);
控制台输出如下:
可以看到,这里已经解决了循环引用的问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)