2021-06-17 15:18:06
JavaScript深浅拷贝的实现及底层原理
一、深浅拷贝基础概念
基本定义
浅拷贝(Shallow Copy):创建一个新对象,复制原始对象的所有属性值到新对象。如果属性值是引用类型(如对象、数组),则复制的是引用地址,新旧对象共享这些引用类型的值。
深拷贝(Deep Copy):创建一个新对象,并递归地复制原始对象的所有属性值到新对象。对于引用类型的属性值,会创建新的引用类型对象,新旧对象完全独立,互不影响。
为什么需要拷贝
避免副作用:防止对原始对象的修改影响到其他部分的代码。
数据隔离:在不同模块或组件间传递数据时保持数据独立性。
状态管理:在状态管理库(如Redux)中需要不可变数据。
缓存机制:创建数据的快照用于比较或恢复。
拷贝的适用场景
配置对象传递:浅拷贝,配置通常是扁平结构,不需要深拷贝。
状态管理:深拷贝,需要完全独立的状态树。
表单数据处理:深拷贝,防止表单修改影响原始数据。
缓存机制:深拷贝,需要完全独立的数据快照。
组件属性传递:浅拷贝,通常传递的是简单数据或组件引用。
二、浅拷贝的实现方式
Object.assign()
方法:用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。
示例:const original = { a: 1, b: { c: 2 } };const copy = Object.assign({}, original);console.log(copy); // { a: 1, b: { c: 2 } }copy.a = 3; // 修改基本类型属性console.log(original.a); // 1 (不受影响)copy.b.c = 4; // 修改引用类型属性console.log(original.b.c); // 4 (受影响,浅拷贝)
特点:
只能拷贝可枚举的自有属性。
对于引用类型的属性,拷贝的是引用地址。
可以接受多个源对象。
展开运算符(...)
方法:ES6引入的展开运算符提供了一种更简洁的浅拷贝方式。
示例:const original = { a: 1, b: { c: 2 } };const copy = { ...original };console.log(copy); // { a: 1, b: { c: 2 } }copy.a = 3; // 修改基本类型属性console.log(original.a); // 1 (不受影响)copy.b.c = 4; // 修改引用类型属性console.log(original.b.c); // 4 (受影响,浅拷贝)
特点:
语法简洁直观。
只能进行一层浅拷贝。
可以方便地合并多个对象。
Array.slice()和Array.concat()
方法:对于数组,可以使用slice()或concat()方法进行浅拷贝。
示例:const originalArray = [1, 2, { a: 3 }];const copyArray1 = originalArray.slice();const copyArray2 = originalArray.concat();console.log(copyArray1); // [1, 2, { a: 3 }]console.log(copyArray2); // [1, 2, { a: 3 }]copyArray1[2].a = 4; // 修改引用类型元素console.log(originalArray[2].a); // 4 (受影响,浅拷贝)
特点:
只能拷贝数组的第一层。
对于数组中的对象元素,拷贝的是引用地址。
不会影响原数组的基本类型元素。
三、浅拷贝的局限性
浅拷贝的主要问题是无法处理嵌套的引用类型:
const original = { a: 1, b: { c: 2, d: [3, 4] }};const copy = { ...original };copy.b.c = 5; // 修改嵌套对象copy.b.d.push(5); // 修改嵌套数组console.log(original.b.c); // 5 (受影响)console.log(original.b.d); // [3, 4, 5] (受影响)四、深拷贝的实现方式
JSON.parse(JSON.stringify())
方法:通过将对象序列化为JSON字符串,再反序列化为新对象。
示例:const original = { a: 1, b: { c: 2, d: [3, 4] }};const copy = JSON.parse(JSON.stringify(original));copy.b.c = 5; // 修改嵌套对象copy.b.d.push(5); // 修改嵌套数组console.log(original.b.c); // 2 (不受影响)console.log(original.b.d); // [3, 4] (不受影响)
优点:
实现简单。
适用于大多数场景。
局限性:
无法处理函数、RegExp、Date等特殊对象。
会丢失undefined和symbol类型的属性。
无法处理循环引用(会抛出错误)。
性能较差(需要序列化和反序列化)。
递归实现深拷贝
方法:手动实现递归深拷贝可以解决JSON方法的一些局限性。
示例:function deepClone(obj, hash = new WeakMap()) { if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof Date) { return new Date(obj); } if (obj instanceof RegExp) { return new RegExp(obj); } if (typeof obj === 'function') { return obj; } if (hash.has(obj)) { return hash.get(obj); } const cloneObj = Array.isArray(obj) ? [] : {}; hash.set(obj, cloneObj); for (const key in obj) { if (obj.hasOwnProperty(key)) { cloneObj[key] = deepClone(obj[key], hash); } } return cloneObj;}const original = { a: 1, b: { c: 2, d: [3, 4] }, e: new Date(), f: /regex/g, g: function() { console.log('hello'); }};const copy = deepClone(original);copy.b.c = 5;copy.b.d.push(5);copy.e.setFullYear(2020);console.log(original.b.c); // 2 (不受影响)console.log(original.b.d); // [3, 4] (不受影响)console.log(original.e.getFullYear()); // 原始年份 (不受影响)
特点:
可以处理日期对象、正则表达式、函数等特殊对象。
可以处理循环引用。
性能较好。
五、底层原理深入分析
JavaScript内存模型
JavaScript中的对象和数组都是引用类型,存储在堆内存中。
变量存储的是对象的引用地址,而不是对象本身。
浅拷贝的底层实现
浅拷贝只是复制了对象的引用地址,新旧对象共享同一个引用类型的值。
修改引用类型的属性会影响原始对象。
深拷贝的底层实现
深拷贝会递归地复制对象的所有属性值,包括嵌套的引用类型。
新旧对象完全独立,互不影响。
性能差异分析
浅拷贝的性能较好,因为它只是复制了引用地址。
深拷贝的性能较差,因为它