数据复制的艺术:深拷贝与浅拷贝在JavaScript中的实现方式

南木元元 2024-06-23 17:05:01 阅读 84

前言

 📫 大家好,我是南木元元,热爱技术和分享,欢迎大家交流,一起学习进步!

 🍅 个人主页:南木元元


目录

赋值和拷贝

浅拷贝与深拷贝区别

浅拷贝的实现方式

1.Object.assign()

2.扩展运算符

3.Array.prototype.slice()

4.Array.prototype.concat()

5.手写实现浅拷贝

深拷贝的实现方式

1.JSON.stringify()

2.函数库lodash

3.手写实现深拷贝

结语


赋值和拷贝

js中的数据类型分为两大类:

基本数据类型引用数据类型

基本数据类型保存在里面,可以直接访问它的值,赋值时,会完整复制变量值。

let a = 10;let b = a; // 赋值b = 20;console.log(a); // 10

上面代码中,两个变量保存了两个不同的内存地址,所以b的改变不会引起a的变化。过程如下:

而引用数据类型保存在里面,栈里面保存的是地址,通过栈里面的地址去访问堆里面的值。

var obj1 = {}var obj2 = obj1;obj2.name = "yuanyuan";console.log(obj1.name); // yuanyuan

 上面代码在赋值时,复制的是栈中的引用地址,两个变量指向堆内存中的同一个对象,所以更改obj2会对obj1产生影响。赋值过程如下:

这是直接赋值的情况,要想两个值互不影响,就需要进行拷贝,js中的拷贝分为浅拷贝和深拷贝。

浅拷贝与深拷贝区别

浅拷贝是按位拷贝对象,它会创建一个新对象,如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址(只拷贝一层)。如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

浅拷贝的实现方式

1.Object.assign()

Object.assign()方法用于将所有可枚举的自有属性从一个或多个源对象复制到目标对象,并返回这个目标对象。可以使用它来实现浅拷贝。

// 初始对象const obj = { name: 'yuanyuan', des: { age: 21, gender: 'male' }};// 使用 Object.assign 进行浅拷贝const obj2 = Object.assign({}, obj);// 修改原始对象中的属性obj.name = '元元';obj.des.age = 22;// 输出console.log(obj); // { name: '元元', des: { age: 22, gender: 'male' } }console.log(obj2); // { name: 'yuanyuan', des: { age: 22, gender: 'male' } }

在上述示例中,Object.assign()创建了一个新对象 obj2并复制了obj中的所有属性。修改obj.name 只影响初始对象obj,不会影响拷贝后的对象,因为字符串是基本数据类型,被复制的是值。

但是,对于初始对象obj的des的修改影响了拷贝后对象obj2的des,因为对象的des属性是一个对象,而Object.assign()复制的是这个对象的引用。因此,当修改obj.des.age时,由于obj.des和obj2.des引用的是同一个对象,所以拷贝后的对象obj2中的des也会被改变。

从这个例子可以看到,浅拷贝是按位拷贝对象,仅拷贝一层,适用于那些不包含深层次嵌套对象的情况。

2.扩展运算符

扩展运算符是一个es6的特性,它提供了一种非常方便的方式来执行浅拷贝,与object.assign实现的浅拷贝相同。

let obj1 = {a:1,b:{c:1}}let obj2 = {...obj1};obj1.a = 2;console.log(obj1); //{a:2,b:{c:1}}console.log(obj2); //{a:1,b:{c:1}}obj1.b.c = 2;console.log(obj1); //{a:2,b:{c:2}}console.log(obj2); //{a:1,b:{c:2}}

3.Array.prototype.slice()

Array.prototype.slice()方法可以从已有数组中返回选定的元素,不会改变原始数组。

let arr = [1, 2, 3];let newArr = arr.slice();newArr[1] = 100;console.log(arr);//[ 1, 2, 3 ]

4.Array.prototype.concat()

Array.prototype.concat()方法用于合并两个或多个数组,此方法不会更改原始数组,而是返回一个新数组。

let arr = [1, 2, 3];let newArr = arr.concat();newArr[1] = 100;console.log(arr);//[ 1, 2, 3 ]

5.手写实现浅拷贝

实现浅拷贝的思路:

对基础类型做最基本的拷贝;

对引用类型开辟新的存储,并且拷贝一层对象属性。

//实现浅拷贝function shallowCopy (obj){ // 只拷贝对象,基本类型或null直接返回 if(typeof obj !== 'object' || obj === null) { return obj; } // 判断是新建一个数组还是对象 let newObj = Array.isArray(obj) ? []: {}; //for…in会遍历对象的整个原型链,如果只考虑对象本身的属性,需要搭配hasOwnProperty for(let key in obj ){ //hasOwnProperty判断是否是对象自身属性,会忽略从原型链上继承的属性 if(obj.hasOwnProperty(key)){ newObj[key] = obj[key];//只拷贝对象本身的属性 } } return newObj;}

深拷贝的实现方式

1.JSON.stringify()

JSON.parse(JSON.stringify());

JSON.parse(JSON.stringify(obj))方法是常用的深拷贝方法之一,它的原理就是利用JSON.stringify将JavaScript对象序列化成为JSON字符串,并将对象里面的内容转换成字符串,再使用JSON.parse来反序列化,将字符串生成一个新的JavaScript对象。

let obj1 = { a: 0, b: { c: 0 }};let obj2 = JSON.parse(JSON.stringify(obj1));obj1.a = 1;obj1.b.c = 1;console.log(obj1); // {a: 1, b: {c: 1}}console.log(obj2); // {a: 0, b: {c: 0}}

这个方法虽然简单,但也存在一些问题:

⽆法解决循环引用问题无法拷贝一些特殊的对象,如 RegExp (会变成空对象)、 Date (被转成字符串)无法拷贝函数

function Obj() { this.func = function () { alert(1) }; this.obj = {a:1}; this.arr = [1,2,3]; this.und = undefined; this.reg = /123/; this.date = new Date(0); this.NaN = NaN; this.infinity = Infinity; this.sym = Symbol(1);}let obj1 = new Obj();Object.defineProperty(obj1,'innumerable',{ enumerable:false, value:'innumerable'});console.log('obj1',obj1);let str = JSON.stringify(obj1);let obj2 = JSON.parse(str);console.log('obj2',obj2);

结果如下: 

2.函数库lodash

可以使用一些第三方库如lodash,它提供了_.cloneDeep用来做深拷贝,可以直接引入并使用:

var _ = require('lodash');var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3]};var obj2 = _.cloneDeep(obj1);console.log(obj1.b.f === obj2.b.f);// false

3.手写实现深拷贝

实现深拷贝的思路就是,使用for in来遍历传入参数的属性值,如果值是基本类型就直接复制,如果是引用类型就进行递归调用该函数,实现代码如下:

function deepCopy (obj, map = new WeakMap()){ // 基本类型或null直接返回 if(typeof obj !== 'object' || obj === null) { return obj; } if (obj instanceof Date) return new Date(obj); if (obj instanceof RegExp) return new RegExp(obj); // 判断是新建一个数组还是对象 let newObj = Array.isArray(obj) ? []: {}; //利用map解决循环引用 if (map.has(obj)) { return map.get(obj); } map.set(obj, newObj);//将当前对象作为key,克隆对象作为value for(let key in obj ){ if(obj.hasOwnProperty(key)){ newObj[key] = deepCopy(obj[key], map);//递归 } } return newObj}

结语

🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏、✍️评论,支持一下博主~ 

 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。