前言
在日常的 JavaScript 开发工作中,我们经常会遇到需要复制对象或数组的情况。比如,你想要修改一个用户信息对象,但又不想影响到原始数据;或者你需要基于现有的配置创建一个新的配置对象。这时候,数据拷贝就显得非常重要了。
本文将帮你彻底理解浅拷贝和深拷贝的概念、原理、实现方法和应用场景,让你在实际开发中能够游刃有余地处理各种数据拷贝需求。
一、前置:JS 的数据类型与赋值操作
(一)JS数据类型与内存存储
要理解拷贝的本质,我们首先需要搞清楚 JavaScript 中数据是如何存储的。我在《深入理解JS(二) - 堆栈与数据类型》这篇文章中详细介绍过,这里简要回顾一下。JavaScript 的数据类型可以分为两大类:
-
基本数据类型(原始类型):
- Number(数字):如
42,3.14 - String(字符串):如
"hello",'world' - Boolean(布尔值):
true或false - Null:表示空值
- Undefined:表示未定义
- Symbol:ES6 新增的唯一标识符
- BigInt:ES2020 新增的大整数类型
- Number(数字):如
-
引用数据类型(复杂类型):
- Object(对象):如
{ name: 'Alice' } - Array(数组):如
[1, 2, 3] - Function(函数):如
function() {} - Date(日期):如
new Date() - RegExp(正则表达式):如
/test/g - 以及其他内置对象类型
- Object(对象):如
-
关键区别在于存储方式:
-
基本数据类型直接存储在栈内存中。想象栈内存就像一个书架,每个变量就是书架上的一个格子,格子里直接放着数据的值。当你复制一个基本类型的变量时,就相当于把这个值完整地复制到另一个格子里,两个格子里的内容是完全独立的。
-
引用数据类型则存储在堆内存中,而变量中保存的只是一个"地址"(引用)。想象堆内存就像一个大仓库,对象存放在仓库里,而变量中保存的是这个仓库的地址。当你复制一个引用类型的变量时,复制的只是这个地址,而不是仓库里的实际内容。这就是为什么修改复制后的对象会影响原对象的根本原因。
-
(二)JS赋值操作的缺陷
为了理解为什么需要拷贝,我们先来看看简单的赋值操作是如何工作的,然后我们就能明白它为什么不能满足我们的需求了。
-
基本数据类型:
// 基本数据类型的赋值 let a = 10; // a 指向栈内存中存储值 10 的位置 let b = a; // b 获得了 a 的值的副本,也是 10 b = 20; // 修改 b 的值为 20 console.log(a); // 输出 10,a 不受影响 console.log(b); // 输出 20 // 这里发生了什么? // 1. a 和 b 是两个完全独立的变量 // 2. 它们各自在栈内存中有自己的存储空间 // 3. 修改其中一个不会影响另一个 -
引用数据类型:
// 引用数据类型的赋值 let obj1 = { name: 'Alice', age: 25 }; // obj1 保存指向堆内存中对象的地址 let obj2 = obj1; // obj2 获得了 obj1 的地址副本 obj2.name = 'Bob'; // 通过 obj2 修改对象的属性 console.log(obj1.name); // 输出 'Bob',obj1 受到了影响! console.log(obj2.name); // 输出 'Bob' // 这里发生了什么? // 1. obj1 和 obj2 保存的是同一个内存地址 // 2. 它们指向堆内存中的同一个对象 // 3. 通过任何一个变量修改对象,都会影响到另一个变量 // 4. 因为它们实际上操作的是同一个对象
这个例子清楚地展示了为什么我们需要拷贝:当我们想要一个对象的独立副本时,简单的赋值是不够的。
二、浅拷贝(Shallow Copy)
(一)什么是浅拷贝?
浅拷贝就像是给一本书做目录复印。你复印了目录页,但是目录中提到的具体章节内容还是指向原书的页面。
具体来说,浅拷贝会创建一个新的对象,并将原对象的所有属性复制到新对象中。但是:
- 如果属性值是基本数据类型,会复制实际的值
- 如果属性值是引用数据类型,只会复制引用地址
这意味着浅拷贝只解决了第一层的独立性问题(基本数据类型完全独立,引用数据类型不独立),对于嵌套的对象或数组,仍然存在共享引用的问题。
(二)浅拷贝的特点
-
对象的层级结构:为了更好地理解浅拷贝的特点,我们先来通过对象的层级结构,明确什么是 "第一层"和"嵌套层"
const user = { // 第一层属性(直接属于 user 对象的属性) name: 'Alice', // 基本类型 age: 25, // 基本类型 // 第一层属性,但值是引用类型 hobbies: ['reading', 'swimming'], // 数组(引用类型) // 第一层属性,但值是对象(引用类型) address: { // 这是第一层 // 第二层属性(嵌套在 address 对象内部的属性) city: 'Beijing', // 这是第二层 district: 'Chaoyang', // 这是第二层 // 第二层属性,但值又是对象 coordinates: { // 这是第二层 // 第三层属性(嵌套在 coordinates 对象内部) lat: 39.9042, // 这是第三层 lng: 116.4074 // 这是第三层 } } }; -
特点:
-
1. 第一层基本类型属性完全独立:
- 像
name、age这样直接属于对象的基本类型属性 - 修改拷贝对象的这些属性不会影响原对象
- 像
-
2. 第一层引用类型属性共享引用:
- 像
hobbies、address这样的第一层属性会被复制 - 但它们指向的数组或对象内容是共享的
- 修改
address.city或hobbies[0]会影响原对象
- 像
-
3. 性能较好:只需要遍历对象的直接属性,不需要递归处理嵌套内容
-
4. 内存占用少:嵌套的对象和数组不会重复创建,节省内存空间
-
(三)浅拷贝的实现方法
1. Object.assign() 方法
Object.assign() 是 ES6 提供的官方方法,用于将一个或多个源对象的属性复制到目标对象。
const original = {
name: 'Alice', // 基本类型
age: 25, // 基本类型
hobbies: ['reading', 'swimming'], // 引用类型(数组)
address: { // 引用类型(对象)
city: 'Beijing',
district: 'Chaoyang'
}
};
// 使用 Object.assign() 进行浅拷贝
const shallowCopy = Object.assign({}, original);
// 测试第一层属性的独立性
shallowCopy.name = 'Bob';
shallowCopy.age = 30;
console.log('原对象 name:', original.name); // 'Alice' - 不受影响
console.log('拷贝对象 name:', shallowCopy.name); // 'Bob'
console.log('原对象 age:', original.age); // 25 - 不受影响
console.log('拷贝对象 age:', shallowCopy.age); // 30
// 测试嵌套对象的共享性
shallowCopy.address.city = 'Shanghai';
console.log('原对象 city:', original.address.city); // 'Shanghai' - 受到影响!
console.log('拷贝对象 city:', shallowCopy.address.city); // 'Shanghai'
// 测试嵌套数组的共享性
shallowCopy.hobbies.push('coding');
console.log('原对象 hobbies:', original.hobbies); // ['reading', 'swimming', 'coding'] - 受到影响!
console.log('拷贝对象 hobbies:', shallowCopy.hobbies); // ['reading', 'swimming', 'coding']
- 为什么会这样?
name和age是基本类型,Object.assign()复制了它们的实际值address和hobbies是引用类型,Object.assign()只复制了它们的引用地址- 所以
original.address和shallowCopy.address指向同一个对象 - 修改其中一个会影响另一个
2. 扩展运算符(Spread Operator)
扩展运算符 ... 是 ES6 引入的语法,提供了一种更简洁的浅拷贝方式。
const original = {
name: 'Alice',
skills: ['JavaScript', 'Python'],
info: { age: 25, city: 'Beijing' }
};
// 使用扩展运算符进行浅拷贝
const shallowCopy = { ...original };
// 效果与 Object.assign() 完全相同
shallowCopy.name = 'Bob';
console.log(original.name); // 'Alice' - 不受影响
shallowCopy.info.age = 30;
console.log(original.info.age); // 30 - 受到影响!
// 扩展运算符的优势:语法更简洁,可读性更好
// 还可以在拷贝的同时添加或覆盖属性
const enhancedCopy = {
...original,
name: 'Charlie', // 覆盖原有属性
email: 'charlie@example.com' // 添加新属性
};
3. 针对数组的浅拷贝方法
数组作为特殊的对象,有自己专门的浅拷贝方法:
const originalArray = [1, 2, [3, 4], { name: 'Alice' }];
// 方法1:Array.from()
const shallowCopy1 = Array.from(originalArray);
// 方法2:slice() 方法
const shallowCopy2 = originalArray.slice();
// 方法3:扩展运算符
const shallowCopy3 = [...originalArray];
// 方法4:concat() 方法
const shallowCopy4 = [].concat(originalArray);
// 测试效果(所有方法效果相同)
console.log('原数组:', originalArray); // [1, 2, [3, 4], { name: 'Alice' }]
console.log('拷贝数组:', shallowCopy1); // [1, 2, [3, 4], { name: 'Alice' }]
// 修改第一层元素
shallowCopy1[0] = 999;
console.log('原数组第一个元素:', originalArray[0]); // 1 - 不受影响
console.log('拷贝数组第一个元素:', shallowCopy1[0]); // 999
// 修改嵌套数组
shallowCopy1[2].push(5);
console.log('原数组嵌套数组:', originalArray[2]); // [3, 4, 5] - 受到影响!
console.log('拷贝数组嵌套数组:', shallowCopy1[2]); // [3, 4, 5]
// 修改嵌套对象
shallowCopy1[3].name = 'Bob';
console.log('原数组嵌套对象:', originalArray[3].name); // 'Bob' - 受到影响!
console.log('拷贝数组嵌套对象:', shallowCopy1[3].name); // 'Bob'
(四)手写浅拷贝函数
理解原理后,我们可以手动实现一个浅拷贝函数:
function shallowCopy(obj) {
// 处理非对象类型(基本类型、null、函数等)
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 处理数组
if (Array.isArray(obj)) {
const arrCopy = [];
for (let i = 0; i < obj.length; i++) {
arrCopy[i] = obj[i]; // 只复制第一层
}
return arrCopy;
// 或者简单地使用:return [...obj];
}
// 处理普通对象
const objCopy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 只复制自有属性,不包括继承的属性
objCopy[key] = obj[key]; // 只复制第一层
}
}
return objCopy;
}
// 测试我们的浅拷贝函数
const original = {
name: 'Alice',
hobbies: ['reading'],
address: { city: 'Beijing' }
};
const copied = shallowCopy(original);
copied.name = 'Bob'; // 不影响原对象
copied.address.city = 'Shanghai'; // 影响原对象
console.log(original.name); // 'Alice'
console.log(original.address.city); // 'Shanghai' - 被影响了
(五)浅拷贝的应用场景
浅拷贝在以下场景中非常有用:
1. React 状态管理
在 React 中,状态更新需要创建新的对象来触发重新渲染:
// React 函数组件示例
function UserProfile() {
const [user, setUser] = useState({
name: 'Alice',
email: 'alice@example.com',
preferences: {
theme: 'dark',
language: 'en'
}
});
// 错误的做法:直接修改状态
const updateNameWrong = (newName) => {
user.name = newName; // 这样不会触发重新渲染
setUser(user);
};
// 正确的做法:使用浅拷贝创建新对象
const updateName = (newName) => {
setUser({
...user, // 浅拷贝原有属性
name: newName // 覆盖特定属性
});
};
// 更新嵌套对象时需要特别注意
const updateTheme = (newTheme) => {
setUser({
...user,
preferences: {
...user.preferences, // 也需要浅拷贝嵌套对象
theme: newTheme
}
});
};
}
- 注意:使用扩展运算符时,应该将其放在前面,使得后面修改的属性值可以覆盖原有的属性值。如果后面修改的属性值放在前面,则会被原有属性值覆盖。
2. 数组操作
避免直接修改原数组,保持数据的不可变性:
const originalList = [
{ id: 1, name: 'Apple', price: 1.2 },
{ id: 2, name: 'Banana', price: 0.8 },
{ id: 3, name: 'Orange', price: 1.5 }
];
// 添加新项目(不修改原数组)
const addItem = (list, newItem) => {
return [...list, newItem]; // 浅拷贝并添加
};
// 删除项目(不修改原数组)
const removeItem = (list, itemId) => {
return list.filter(item => item.id !== itemId); // filter 返回新数组
};
// 更新项目(不修改原数组)
const updateItem = (list, itemId, updates) => {
return list.map(item =>
item.id === itemId
? { ...item, ...updates } // 浅拷贝并更新
: item
);
};
// 使用示例
const newList = addItem(originalList, { id: 4, name: 'Grape', price: 2.0 });
console.log('原列表长度:', originalList.length); // 3 - 不变
console.log('新列表长度:', newList.length); // 4
三、深拷贝(Deep Copy)
(一)什么是深拷贝?
如果说浅拷贝像是复印书的目录,那么深拷贝就像是把整本书完整地复印一遍,包括所有的章节内容。
深拷贝会递归地复制对象的所有层级,创建一个完全独立的副本。无论原对象的结构多么复杂,有多少层嵌套,深拷贝都会为每一层创建新的对象或数组。这样,修改拷贝后的对象的任何部分都不会影响原对象。
(二)深拷贝的特点
-
- 完全独立:拷贝对象与原对象完全独立,互不影响
-
- 递归复制:会递归地复制所有嵌套的对象和数组
-
- 内存占用大:会创建所有嵌套对象的副本,占用更多内存
-
- 性能开销大:需要遍历所有层级,执行时间较长
-
- 处理复杂:需要考虑循环引用、特殊对象类型等问题
(三)深拷贝的实现方法
1. JSON.parse(JSON.stringify()) 方法
这是最简单也是最常用的深拷贝方法,利用 JSON 的序列化和反序列化来实现。
-
JSON 方法的工作原理:
-
JSON.stringify(original)将对象转换为 JSON 字符串
-
JSON.parse()将 JSON 字符串解析回对象
-
- 由于 JSON 不支持引用,所以得到的是完全独立的对象
-
-
示例:
const original = { name: 'Alice', age: 25, hobbies: ['reading', 'swimming'], address: { city: 'Beijing', district: 'Chaoyang', coordinates: { latitude: 39.9042, longitude: 116.4074 } } }; // 使用 JSON 方法进行深拷贝 const deepCopy = JSON.parse(JSON.stringify(original)); // 测试完全独立性 deepCopy.name = 'Bob'; deepCopy.address.city = 'Shanghai'; deepCopy.address.coordinates.latitude = 31.2304; deepCopy.hobbies.push('coding'); console.log('原对象 name:', original.name); // 'Alice' - 不受影响 console.log('拷贝对象 name:', deepCopy.name); // 'Bob' console.log('原对象 city:', original.address.city); // 'Beijing' - 不受影响 console.log('拷贝对象 city:', deepCopy.address.city); // 'Shanghai' console.log('原对象 latitude:', original.address.coordinates.latitude); // 39.9042 - 不受影响 console.log('拷贝对象 latitude:', deepCopy.address.coordinates.latitude); // 31.2304 console.log('原对象 hobbies:', original.hobbies); // ['reading', 'swimming'] - 不受影响 console.log('拷贝对象 hobbies:', deepCopy.hobbies); // ['reading', 'swimming', 'coding'] -
JSON 方法的局限性:
虽然 JSON 方法简单易用,但它有很多局限性,主要包括:
-
1. 不支持的数据类型:
- 函数:会被完全丢失
- undefined:会被忽略
- Symbol:会被忽略
- NaN 和 Infinity:会被转换为
null
-
2. 特殊对象类型处理不当:
- Date 对象:会被转换为字符串,失去 Date 类型
- RegExp 对象:会被转换为空对象
{} - Map 和 Set:会被转换为空对象
{}
-
3. 循环引用问题:
- 当对象存在循环引用时,会抛出错误
-
示例:让我们通过代码来验证这些局限性。
-
数据类型不支持或特殊对象处理不当:
// 测试各种数据类型的处理 const complexObj = { // 支持的类型 name: 'Alice', age: 25, // 不支持的类型 greet: function() { return `Hello, I'm ${this.name}`; }, // 函数 undefinedValue: undefined, // undefined symbolKey: Symbol('test'), // Symbol notANumber: NaN, // NaN infinity: Infinity, // Infinity // 特殊对象类型 birthday: new Date('1998-01-01'), // Date pattern: /test/g, // RegExp map: new Map([['key1', 'value1']]), // Map set: new Set([1, 2, 3]) // Set }; const jsonCopy = JSON.parse(JSON.stringify(complexObj)); console.log('原对象类型检查:'); console.log('函数存在:', typeof complexObj.greet === 'function'); // true console.log('Date类型:', complexObj.birthday instanceof Date); // true console.log('RegExp类型:', complexObj.pattern instanceof RegExp); // true console.log('\nJSON拷贝后的变化:'); console.log('函数丢失:', jsonCopy.greet); // undefined console.log('Date变字符串:', typeof jsonCopy.birthday); // "string" console.log('RegExp变空对象:', JSON.stringify(jsonCopy.pattern)); // "{}" console.log('NaN变null:', jsonCopy.notANumber); // null -
循环引用测试:
const obj = { name: 'test' }; obj.self = obj; // 创建循环引用 try { const copy = JSON.parse(JSON.stringify(obj)); } catch (error) { console.error('JSON方法无法处理循环引用:', error.message); // 输出:Converting circular structure to JSON }
-
2. 手写深拷贝函数
为了解决 JSON 方法的局限性,我们可以手写一个深拷贝函数。
-
核心要点:手写的深拷贝函数需要符合以下几个关键点。
- 递归处理:对每一层的对象和数组都进行递归拷贝
- 循环引用处理:使用 WeakMap 记录已拷贝的对象,避免无限递归
- 特殊类型处理:正确处理 Date、RegExp、Map、Set 等特殊对象
- 类型判断:准确识别不同的数据类型并采用相应的拷贝策略
-
示例代码:
/** * 深拷贝函数 - 递归复制对象的所有层级 * @param {*} obj - 需要拷贝的对象 * @param {WeakMap} hash - 用于记录已拷贝对象的映射表,防止循环引用 * @returns {*} 深拷贝后的新对象 */ function deepClone(obj, hash = new WeakMap()) { // 第一步:处理基本类型和 null // 基本类型(number、string、boolean等)直接返回,无需拷贝 if (obj === null || typeof obj !== 'object') return obj; // 第二步:检查循环引用 // 如果当前对象已经被拷贝过,直接返回之前的拷贝结果 // 这样可以避免 A->B->A 这种循环引用导致的无限递归 if (hash.has(obj)) return hash.get(obj); // 第三步:根据构造函数判断对象类型 // 通过 constructor 属性可以准确识别对象的具体类型 const Constructor = obj.constructor; // 第四步:根据不同类型采用相应的拷贝策略 switch (Constructor) { case Date: // Date 对象:创建新的 Date 实例,保持相同的时间值 return new Date(obj); case RegExp: // 正则表达式:创建新的 RegExp 实例,保持相同的模式和标志 return new RegExp(obj); case Map: // Map 对象处理 const mapCopy = new Map(); // 先将空的 Map 存入 hash,防止循环引用 hash.set(obj, mapCopy); // 遍历原 Map,递归拷贝每个值(键通常是基本类型,不需要拷贝) obj.forEach((value, key) => { mapCopy.set(key, deepClone(value, hash)); }); return mapCopy; case Set: // Set 对象处理 const setCopy = new Set(); // 先将空的 Set 存入 hash,防止循环引用 hash.set(obj, setCopy); // 遍历原 Set,递归拷贝每个值 obj.forEach(value => { setCopy.add(deepClone(value, hash)); }); return setCopy; case Array: // 数组处理 const arrCopy = []; // 先将空数组存入 hash,防止循环引用 hash.set(obj, arrCopy); // 遍历原数组,递归拷贝每个元素 obj.forEach((item, index) => { arrCopy[index] = deepClone(item, hash); }); return arrCopy; default: // 普通对象处理(包括自定义对象、函数等) const objCopy = {}; // 先将空对象存入 hash,防止循环引用 hash.set(obj, objCopy); // 遍历对象的所有可枚举属性,递归拷贝每个属性值 Object.keys(obj).forEach(key => { objCopy[key] = deepClone(obj[key], hash); }); return objCopy; } } -
测试验证:
// 测试数据 const testObj = { name: 'Alice', date: new Date('2025-08-11'), regex: /test/gi, map: new Map([['key1', 'value1']]), set: new Set([1, 2, 3]), nested: { array: [1, { deep: 'value' }], level: 2 } }; // 添加循环引用 testObj.self = testObj; const cloned = deepClone(testObj); // 验证独立性 cloned.name = 'Bob'; cloned.nested.level = 3; cloned.map.set('key2', 'value2'); console.log('原对象 name:', testObj.name); // 'Alice' console.log('拷贝对象 name:', cloned.name); // 'Bob' console.log('原对象 level:', testObj.nested.level); // 2 console.log('拷贝对象 level:', cloned.nested.level); // 3 console.log('循环引用处理:', cloned.self === cloned); // true
3. 使用第三方库
在生产环境中,推荐使用经过充分测试的第三方库,处理深拷贝最常用的第三方库是 Lodash,其中的 _.cloneDeep() 方法是深拷贝场景下的行业标准之一,被广泛应用于各类项目中。
-
Lodash的
_.cloneDeep():const _ = require('lodash'); const original = { user: { name: 'Alice', settings: { theme: 'dark' } }, data: [1, 2, { nested: true }], date: new Date(), regex: /pattern/g }; const cloned = _.cloneDeep(original); // 修改不会影响原对象 cloned.user.name = 'Bob'; cloned.data[2].nested = false; console.log(original.user.name); // 'Alice' console.log(cloned.user.name); // 'Bob'
(四)深拷贝的应用场景
深拷贝在以下场景中是必需的,这些场景要求对象的完全独立性:
1. 保存数据快照(如表单回滚、历史记录)
当需要保存数据在某一时刻的"快照",以便后续对比、回滚或查看历史时,必须使用深拷贝。
-
典型场景:用户填写复杂表单(含嵌套字段,如
{ user: { name: "", address: { city: "" } } })时,需要在提交前保存原始数据。如果用浅拷贝,修改表单时原始快照会被同步修改,导致无法回滚到初始状态。// 表单管理器示例 class FormManager { constructor(initialData) { this.currentData = initialData; this.originalSnapshot = deepClone(initialData); // 深拷贝保存原始快照 } rollback() { this.currentData = deepClone(this.originalSnapshot); return this.currentData; } }
深拷贝能确保快照数据与当前操作的数据完全隔离,实现真正的数据回滚功能。
2. 状态管理(如框架中的不可变状态)
在 React、Vue 等框架中,状态通常被设计为"不可变"——不直接修改原状态,而是创建新状态替换旧状态。
-
核心问题:修改嵌套的状态数据
state = { list: [{ id: 1, info: { name: "a" } }] }时,若用浅拷贝复制list,修改其中info.name会同时影响原状态,导致框架无法检测到状态变化(因为引用地址未变)。// 错误:浅拷贝无法满足不可变原则 const newState = { ...state }; // 浅拷贝 newState.list[0].info.name = 'new name'; // 影响了原状态! // 正确:深拷贝确保完全不可变 const newState = deepClone(state); newState.list[0].info.name = 'new name'; // 完全独立
深拷贝能创建全新的嵌套对象,确保状态更新符合"不可变"原则,触发正确的重新渲染。
3. 处理可复用的配置对象
当需要基于一个"基础配置"创建多个变体配置时,深拷贝可以避免变体间的相互影响。
-
典型场景:有一个基础主题配置
baseTheme = { color: "red", font: { size: 14 } },需要创建darkTheme和lightTheme两个变体。若用浅拷贝,修改darkTheme.font.size会同步修改baseTheme的font.size,导致基础配置被污染。// 正确做法:深拷贝确保配置独立 function createTheme(baseTheme, overrides) { const theme = deepClone(baseTheme); // 深拷贝 return Object.assign(theme, overrides); } const darkTheme = createTheme(baseTheme, { color: 'black' }); const lightTheme = createTheme(baseTheme, { color: 'white' });
深拷贝能让每个变体配置都是独立的副本,安全地修改各自的嵌套属性。
4. 避免函数参数的意外修改
当函数接收一个对象作为参数,且需要在函数内部修改该对象时,深拷贝可以防止修改影响函数外部的原对象。
-
典型场景:函数
formatData(data)需要对传入的data(含嵌套数组data.items = [{...}])进行格式化处理。若直接操作原对象,函数外部的data会被同步修改,可能导致不可预期的副作用。function formatData(data) { const formattedData = deepClone(data); // 深拷贝防止修改原数据 // 安全地进行各种格式化操作 formattedData.items.forEach(item => { item.price = `¥${item.price.toFixed(2)}`; }); return formattedData; }
深拷贝后再处理,能保证原数据的完整性,避免函数的副作用。
5. 复杂数据的序列化/持久化
在将对象转换为字符串(如 JSON.stringify)进行存储(如 localStorage)或传输时,若对象中存在循环引用或特殊类型(如 Date、RegExp),原生序列化可能丢失信息,此时常需结合深拷贝进行预处理。
-
核心价值:深拷贝时可将
Date对象转换为字符串保存,还原时再解析,确保数据序列化后能正确恢复。// 序列化预处理 function prepareForSerialization(obj) { return deepClone(obj, (key, value) => { if (value instanceof Date) { return { __type: 'Date', value: value.toISOString() }; } return value; }); }
通过深拷贝结合自定义处理逻辑,可以安全地序列化和恢复复杂数据结构。
四、性能差异
JS 中浅拷贝和深拷贝的性能差异主要源于处理数据的范围不同,核心区别如下:
-
浅拷贝:仅复制对象表层属性,对嵌套的引用类型(如对象、数组)只复制引用地址。因此操作简单,无需递归,时间开销极小(时间复杂度为表层属性数量的 O (n)),空间开销也很小(仅新增表层对象,嵌套数据共享内存)。
-
深拷贝:需递归遍历所有嵌套层级,为每个引用类型创建全新副本。因此时间开销大(时间复杂度为所有层级总属性数的 O (n),嵌套越深开销越大),空间开销也高(需为所有嵌套数据分配新内存,可能接近原对象总大小),且需处理循环引用、特殊类型等边缘情况,进一步增加性能成本。
简言之:浅拷贝因操作范围有限而高效,深拷贝因全量递归处理而开销大,数据越复杂(嵌套深、规模大),两者性能差距越明显。
结语
浅拷贝和深拷贝是 JavaScript 开发中的基础概念,但它们的重要性往往被低估。正确理解和使用这两种拷贝方式,不仅能帮助我们避免许多隐蔽的 bug,还能在性能和内存使用上做出更明智的选择。掌握了浅拷贝和深拷贝的原理和应用,你就拥有了处理复杂数据操作的强大工具。
希望这篇文章有帮助到你,如果文章有错误,请你在评论区指出,大家一起进步,谢谢🙏。