JavaScript 拷贝全解析:从浅拷贝到深拷贝的完整指南
引言
在Javascript开发中,数据拷贝是我们每天都会遇到的基础操作。然而,这个看似简单的概念背后隐藏着许多陷阱和细节。错误的数据拷贝可能导致难以调试的bug、内存泄漏甚至程序崩溃。
你是否曾遇到过:
- 修改一个对象后,另一个"独立"的对象也被意外修改?
- 尝试复制包含函数、Date对象或循环引用的数据结构时失败?
- 在处理大型数据集时,拷贝导致性能急剧下降?
本文将从基础概念出发,深入探讨JavaScript中的各种拷贝技术,提供完整的实现方案,并帮助你根据不同的场景选择最合适的拷贝策略。
一、理解JavaScript的数据类型
在深入拷贝之前,我们需要先理解JavaScript的数据类型,因为不同类型的数据在拷贝时有根本性的区别。
1.1 基本类型(Primitive Types)
JavaScript有7种基本数据类型:
// 基本类型 - 按值存储,拷贝时直接复制值
const str = 'Hello'; // String
const num = 42; // Number
const bool = true; // Boolean
const nullValue = null; // Null
const undefinedValue; // Undefined
const sym = Symbol('id'); // Symbol(ES6)
const bigInt = 123n; // BigInt(ES2020)
1.2 引用类型(Reference Types)
// 引用类型 - 按引用存储,拷贝时复制引用
const obj = { name: 'John' }; // Object
const arr = [1, 2, 3]; // Array
const func = () => {}; // Function
const date = new Date(); // Date
const regex = /pattern/gi; // RegExp
const map = new Map(); // Map
const set = new Set(); // Set
1.3 内存模型图解
// 基本类型 - 栈内存存储
let a = 10; // 栈: a = 10
let b = a; // 栈: b = 10 (值的拷贝)
b = 20; // 栈: b = 20, a 保持不变
// 引用类型 - 堆内存存储
let obj1 = { x: 10 }; // 栈: obj1 -> 堆地址1 {x: 10}
let obj2 = obj1; // 栈: obj2 -> 同一个堆地址1
obj2.x = 20; // 堆地址1: {x: 20}, obj1.x 也变为 20
理解这个区别是掌握拷贝技术的基础。接下来,我们开始探讨具体的拷贝方法。
二、浅拷贝(Shallow Copy)
浅拷贝创建一个新对象,复制原始对象的所有属性值到新对象。如果属性值是基本类型,则复制值; 如果是引用类型,则复制引用。
2.1 对象浅拷贝方法
方法1: 展开运算符(Spread Operator) - ES6+
const original = {
name: 'John',
age: 30,
hobbies: ['reading', 'gaming'],
address: {
city: 'Shang Hai',
zip: '120001'
}
};
const shallowCopy = { ...original };
// 测试
console.log(shallowCopy === original); // false - 是新对象
console.log(shallowCopy.hobbies === original.hobbies); // true - 引用相同
console.log(shallowCopy.address === original.address); // true - 引用相同
// 修改嵌套对象会影响原对象
shallowCopy.hobbies.push("coding");
console.log(original.hobbies); // [ 'reading', 'gaming', 'coding' ] - 被影响
方法2: Object.assign() - ES6
const shallowCopy2 = Object.assign({}, original);
// Object.assign 可以合并多个对象
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3 };
const merged = Object.assign({}, obj1, obj2, obj3);
console.log(merged); // { a: 1, b: 2, c: 3 }
方法3: 手动实现浅拷贝
function shallowCopy(obj) {
// 处理 null 和 undefined
if (obj === null || typeof obj !== "object") {
return obj;
}
// 处理数组
if (Array.isArray(obj)) {
return [...obj];
}
// 处理对象
const copy = {};
for (const key in obj) {
// 只拷贝对象自身的属性(不包括原型链上的属性)
if (obj.hasOwnProperty(key)) {
copy[key] = obj[key];
}
}
return copy;
}
// 测试
const testObj = { a: 1, b: { c: 2 } };
const copied = shallowCopy(testObj);
console.log(copied.b === testObj.b); // true - 浅拷贝
方法4: 使用 Object.create() (原型链拷贝)
// 这种方法会保持原型链
function shallowCopyWithPrototype(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
// 创建一个新对象,继承原对象的原型
const copy = Object.create(Object.getPrototypeOf(obj));
// 拷贝自有属性
Object.getOwnPropertyNames(obj).forEach((prop) => {
const descriptor = Object.getOwnPropertyDescriptor(obj, prop);
Object.defineProperty(copy, prop, descriptor);
});
return copy;
}
// 测试
const protoObj = { inherited: "from prototype" };
const objWithProto = Object.create(protoObj);
objWithProto.own = "own prototype";
const copiedWithProto = shallowCopyWithPrototype(objWithProto);
console.log(copiedWithProto.inherited); // from prototype - 继承原型
console.log(copiedWithProto.own); // own prototype
2.2 数组浅拷贝方法
方法1: 展开运算符
const originalArray = [1, 2, 3, { x: 4 }];
const shallowArray = [...originalArray];
// 修改基本类型不会影响原数组
shallowArray[0] = 100;
console.log(originalArray[0]); // 1 - 不受影响
// 修改引用类型会影响原数组
shallowArray[3].x = 400;
console.log(originalArray[3].x); // 400 - 受影响!
方法2: slice()方法
const shallowArray2 = originalArray.slice();
// 效果与展开运算符相同
方法3: concat()方法
const shallowArray3 = originalArray.concat();
// 效果与展开运算符相同
方法4: Array.from() - ES6
const shallowArray4 = Array.from(originalArray);
方法5: 手动实现数组浅拷贝
function shallowCopyArray(arr) {
if (!Array.isArray(arr)) {
throw new TypeError("Excepted an array");
}
const copy = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
copy[i] = arr[i];
}
return copy;
}
2.3 浅拷贝的局限性
浅拷贝的主要问题是:
- 嵌套对象问题: 只拷贝一层,嵌套的对象仍然是共享的
- 对象共享问题: 修改浅拷贝对象的引用类型属性会影响原对象
- 特殊对象的引用共享: 对于Date、RegExp等特殊对象, 浅拷贝只复制引用, 不会创建新实例
三、深拷贝(Deep Copy)
深拷贝会创建一个完全独立的新对象, 递归复制所有嵌套的对象和数组, 使新对象与原对象完全分离。
3.1 使用 JSON 方法(最简单但有限制)
const deepCopyWithJSON = JSON.parse(JSON.stringify(original));
// 测试
const obj = {
name: "John",
date: new Date(),
func: () => console.log("hello"),
undef: undefined,
symbol: Symbol("id"),
infinity: Infinity,
nan: NaN,
regex: /pattern/gi,
set: new Set([1, 2, 3]),
map: new Map([["key", "value"]]),
nested: { a: 1 },
};
const jsonCopy = JSON.parse(JSON.stringify(obj));
console.log(jsonCopy);
// 输出:
// {
// name: 'John',
// date: '2025-12-04T14:13:18.238Z', // Date 变成了字符串
// func 不存在 // 函数被省略
// undef 不存在 // undefined 被省略
// symbol 不存在 // Symbol 被省略
// infinity: null, // Infinity 变成了 null
// nan: null, // NaN 变成了 null
// regex: {}, // RegExp 变成了空对象
// set: {}, // Set 变成了空对象
// map: {}, // Map 变成了空对象
// nested: { a: 1 }
// }
JSON方法的限制:
- 无法拷贝函数
- 无法拷贝
undefined - 无法拷贝
Symbol - 无法拷贝循环引用
- 特殊对象(Date、RegExp、Set、Map等)会被错误处理
- 会忽略原型链
3.2 递归实现深拷贝
基础递归实现
function deepClone(obj, hash = new WeakMap()) {
// 处理基本类型和 null/undefined
if (obj === null || typeof obj !== "object") {
return obj;
}
// 处理 Date 对象
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// 处理正则表达式
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 处理数组
if (Array.isArray(obj)) {
return obj.map((item) => deepClone(item, hash));
}
// 处理普通对象 - 检查循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
const clone = {};
hash.set(obj, clone);
// 拷贝所有属性, 包括不可枚举属性(可选)
const props = Object.getOwnPropertyNames(obj);
for (const prop of props) {
clone[prop] = deepClone(obj[prop], hash);
}
return clone;
}
// 测试基础功能
const testObj = {
name: "Test",
date: new Date(),
regex: /test/gi,
arr: [1, 2, { nested: true }],
nested: {
level1: {
level2: "deep",
},
},
};
const cloned = deepClone(testObj);
console.log(cloned.date instanceof Date); // true
console.log(cloned.regex instanceof RegExp); // true
console.log(cloned.arr[2] === testObj.arr[2]); // false - 深拷贝成功
支持更多数据类型的完整实现
function deepCloneComplete(target, map = new WeakMap()) {
// 基本类型直接返回
if (target === null || typeof target !== "object") {
return target;
}
// 检查循环引用
if (map.has(target)) {
return map.get(target);
}
// 克隆特殊对象类型
// Date
if (target instanceof Date) {
return new Date(target);
}
// RegExp
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
}
// Map
if (target instanceof Map) {
const clone = new Map();
map.set(target, clone);
target.forEach((value, key) => {
clone.set(deepCloneComplete(key, map), deepCloneComplete(value, map));
});
return clone;
}
// Set
if (target instanceof Set) {
const clone = new Set();
map.set(target, clone);
target.forEach((value) => {
clone.add(deepCloneComplete(value, map));
});
return clone;
}
// ArrayBuffer
if (target instanceof ArrayBuffer) {
return target.slice(0);
}
// TypedArrat (Int8Array, Uint8Array, etc.)
if (ArrayBuffer.isView(target)) {
return new target.constructor(target);
}
// Array
if (Array.isArray(target)) {
const arrClone = [];
map.set(target, arrClone);
for (let i = 0; i < target.length; i++) {
arrClone[i] = deepCloneComplete(target[i], map);
}
return arrClone;
}
// 普通对象
const objClone = Object.create(Object.getPrototypeOf(target));
map.set(target, objClone);
// 获取所有属性(包括 Symbol)
const allKeys = Reflect.ownKeys(target);
for (const key of allKeys) {
const descriptor = Object.getOwnPropertyDescriptor(target, key);
if (descriptor) {
if (descriptor.hasOwnProperty("value")) {
// 数据属性
objClone[key] = deepCloneComplete(target[key], map);
} else {
// 访问器属性 (getter/setter)
Object.defineProperty(objClone, key, descriptor);
}
}
}
return objClone;
}
// 测试完整功能
const complexObj = {
string: "hello",
number: 42,
boolean: true,
null: null,
undefined: undefined,
symbol: Symbol("test"),
date: new Date(),
regex: /test/gi,
array: [1, 2, { nested: true }],
map: new Map([["key", { value: "map value" }]]),
set: new Set([1, 2, 3]),
buffer: new ArrayBuffer(8),
uintArray: new Uint8Array([1, 2, 3]),
object: {
nested: {
deeply: "nested value",
},
},
get computed() {
return this.string.toUpperCase();
},
method() {
return this.string;
},
};
// 添加循环引用
complexObj.self = complexObj;
complexObj.circular = { parent: complexObj };
const completeClone = deepCloneComplete(complexObj);
console.log(completeClone.date instanceof Date); // true
console.log(completeClone.regex instanceof RegExp); // true
console.log(completeClone.map instanceof Map); // true
console.log(completeClone.uintArray instanceof Uint8Array); // true
console.log(completeClone.computed); // 'HELLO'
console.log(completeClone.method()); // 'hello'
console.log(completeClone.self === completeClone); // true - 循环引用正确处理
console.log(completeClone.circular.parent === completeClone); // true
3.3 使用 structuredClone API(现代浏览器)
HTML5规范引入了structuredClone()方法, 提供了一种标准化的深拷贝方法。
// 浏览器环境中的使用
const original = {
name: 'John',
date: new Date(),
array: [1, 2, 3],
nested: { value: 'test' }
};
try {
const cloned = structuredClone(original);
console.log(cloned.date instanceof Date); // true
console.log(cloned.nested === original.nested); // false
} catch (err) {
console.log('structuredClone not supported:', err);
}
// Node.js 中的使用(v17+)
if (typeof structuredClone === 'function') {
const cloned = structuredClone(original);
}
// structuredClone 支持的数据类型:
// - 基本类型(除 Symbol)
// - Boolean、Number、String 对象
// - Date
// - RegExp
// - ArrayBuffer、TypedArray
// - Map、Set
// - Array、Object
// - 循环引用
// 不支持:
// - 函数
// - DOM 节点
// - Error 对象
// - 原型链
3.4 使用第三方库
对于生产环境, 使用成熟的第三方库通常是更好的选择:
Lodash的_.cloneDeep
// 使用 Lodash
const _ = require('lodash');
const obj = {
date: new Date(),
regex: /test/gi,
func: () => console.log('hi'),
nested: { a: 1 }
};
const cloned = _.cloneDeep(obj);
console.log(cloned.date instanceof Date); // true
console.log(cloned.regex instanceof RegExp); // true
console.log(typeof cloned.func); // 'function' - 函数被保留
自己实现类似Lodash的cloneDeep
function cloneDeep(value, stack = new Map()) {
// 基本类型直接返回
if (value === null || typeof value !== 'object') {
return value;
}
// 检查循环引用
if (stack.has(value)) {
return stack.get(value);
}
let clone;
// 处理特殊对象
if (value instanceof Date) {
clone = new Date(value.getTime());
stack.set(value, clone);
return clone;
}
if (value instanceof RegExp) {
clone = new RegExp(value.source, value.flags);
stack.set(value, clone);
return clone;
}
if (value instanceof Map) {
clone = new Map();
stack.set(value, clone);
value.forEach((val, key) => {
clone.set(cloneDeep(key, stack), cloneDeep(val, stack));
});
return clone;
}
if (value instanceof Set) {
clone = new Set();
stack.set(value, clone);
value.forEach(val => {
clone.add(cloneDeep(val, stack));
});
return clone;
}
if (Array.isArray(value)) {
clone = [];
stack.set(value, clone);
for (let i = 0; i < value.length; i++) {
clone[i] = cloneDeep(value[i], stack);
}
return clone;
}
// 处理普通对象
clone = Object.create(Object.getPrototypeOf(value));
stack.set(value, clone);
// 拷贝所有属性
for (const key in value) {
if (value.hasOwnProperty(key)) {
clone[key] = cloneDeep(value[key], stack);
}
}
return clone;
}
四、特殊场景和边缘情况
4.1 循环引用处理
循环引用是深拷贝中最棘手的问题之一, 处理不当会导致无限递归和栈溢出
// 循环引用示例
const circularObj = { name: 'Circular' };
circularObj.self = circularObj;
circularObj.ref = { parent: circularObj };
// 处理循环引用的深拷贝实现
function cloneDeepWithCircular(obj, cache = new WeakMap()) {
// 非对象直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 检查缓存中是否已有该对象的拷贝
if (cache.has(obj)) {
return cache.get(obj);
}
// 根据对象类型创建相应的空结构
let clone;
if (obj instanceof Date) {
clone = new Date(obj);
} else if (obj instanceof RegExp) {
clone = new RegExp(obj.source, obj.flags);
} else if (obj instanceof Map) {
clone = new Map();
} else if (obj instanceof Set) {
clone = new Set();
} else if (Array.isArray(obj)) {
clone = [];
} else {
clone = Object.create(Object.getPrototypeOf(obj));
}
// 将空结构存入缓存(在递归前存入,防止无限递归)
cache.set(obj, clone);
// 递归拷贝
if (obj instanceof Map) {
obj.forEach((value, key) => {
clone.set(
cloneDeepWithCircular(key, cache),
cloneDeepWithCircular(value, cache)
);
});
} else if (obj instanceof Set) {
obj.forEach(value => {
clone.add(cloneDeepWithCircular(value, cache));
});
} else if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
clone[i] = cloneDeepWithCircular(obj[i], cache);
}
} else {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = cloneDeepWithCircular(obj[key], cache);
}
}
}
return clone;
}
// 测试循环引用
const testCircular = { a: 1 };
testCircular.b = testCircular;
testCircular.c = { ref: testCircular };
const clonedCircular = cloneDeepWithCircular(testCircular);
console.log(clonedCircular.b === clonedCircular); // true
console.log(clonedCircular.c.ref === clonedCircular); // true
console.log(clonedCircular !== testCircular); // true - 不是同一个对象
4.2 函数拷贝
函数拷贝是一个有争议的话题,因为函数可能依赖于闭包中的外部变量。
// 函数拷贝的几种方法
function cloneFunction(func) {
// 方法1:使用 eval(不推荐,安全问题)
const funcString = func.toString();
// 方法2:使用 Function 构造函数
const clonedFunc = new Function('return ' + funcString)();
// 拷贝函数属性
Object.getOwnPropertyNames(func).forEach(prop => {
if (prop !== 'length' && prop !== 'name' && prop !== 'prototype') {
Object.defineProperty(clonedFunc, prop,
Object.getOwnPropertyDescriptor(func, prop));
}
});
// 拷贝原型
clonedFunc.prototype = func.prototype;
return clonedFunc;
}
// 实际使用中,通常不拷贝函数,而是保留引用
function cloneDeepWithFunction(obj, cache = new Map()) {
if (typeof obj === 'function') {
return obj; // 直接返回函数引用
}
// ... 其他类型的处理
}
// 测试
const objWithFunc = {
name: 'Test',
sayHello: function() {
console.log(`Hello, ${this.name}`);
},
arrowFunc: () => console.log('Arrow')
};
const clonedWithFunc = cloneDeepWithFunction(objWithFunc);
clonedWithFunc.name = 'Cloned';
clonedWithFunc.sayHello(); // Hello, Cloned
4.3 DOM元素拷贝
DOM元素有特殊的拷贝需求:
function cloneDOMElement(element, deep = true) {
// 使用 cloneNode 方法
const cloned = element.cloneNode(deep);
// 处理事件监听器
// 注意:cloneNode 不会拷贝事件监听器
// 处理数据属性
if (element.dataset) {
Object.assign(cloned.dataset, element.dataset);
}
// 处理自定义属性
const attributes = element.attributes;
for (let i = 0; i < attributes.length; i++) {
const attr = attributes[i];
if (attr.name.startsWith('data-') || attr.name.startsWith('aria-')) {
cloned.setAttribute(attr.name, attr.value);
}
}
return cloned;
}
// 使用示例
// const originalDiv = document.getElementById('original');
// const clonedDiv = cloneDOMElement(originalDiv, true);
// document.body.appendChild(clonedDiv);
4.4 性能优化技巧
深拷贝可能成为性能瓶颈,特别是处理大型对象时。
// 性能优化的深拷贝
function fastDeepClone(obj, cache = new WeakMap()) {
// 快速路径:基本类型
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 检查缓存
if (cache.has(obj)) {
return cache.get(obj);
}
let clone;
// 使用构造函数快速创建对象
const Ctor = obj.constructor;
switch (Ctor) {
case Date:
clone = new Date(obj);
break;
case RegExp:
clone = new RegExp(obj);
break;
case Map:
clone = new Map();
cache.set(obj, clone);
obj.forEach((value, key) => {
clone.set(fastDeepClone(key, cache), fastDeepClone(value, cache));
});
return clone;
case Set:
clone = new Set();
cache.set(obj, clone);
obj.forEach(value => {
clone.add(fastDeepClone(value, cache));
});
return clone;
case Array:
clone = new Array(obj.length);
cache.set(obj, clone);
for (let i = 0; i < obj.length; i++) {
clone[i] = fastDeepClone(obj[i], cache);
}
return clone;
default:
// 普通对象
clone = Object.create(Object.getPrototypeOf(obj));
cache.set(obj, clone);
}
// 快速属性拷贝
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
clone[key] = fastDeepClone(obj[key], cache);
}
return clone;
}
// 性能对比
const largeObj = {};
for (let i = 0; i < 10000; i++) {
largeObj[`key${i}`] = {
nested: { value: i },
array: new Array(10).fill(i)
};
}
console.time('JSON 深拷贝');
JSON.parse(JSON.stringify(largeObj));
console.timeEnd('JSON 深拷贝');
console.time('递归深拷贝');
fastDeepClone(largeObj);
console.timeEnd('递归深拷贝');
五、实践应用和最佳实践
5.1 何时使用浅拷贝
适合浅拷贝的场景:
- 简单数据结构: 对象只有一层,没有嵌套
- 性能敏感: 需要快速拷贝,不关心嵌套对象的独立性
- 不可变数据: 数据不会被修改,或修改时创建新对象
- 配置对象: 只需要修改顶层配置
// 浅拷贝适用场景
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
};
// 只需要修改顶层配置时
const devConfig = { ...config, apiUrl: 'https://dev-api.example.com' };
// headers 对象仍然是共享的,但这通常是可以接受的
5.2 何时使用深拷贝
适合深拷贝的场景:
- 复杂嵌套结构: 对象有多层嵌套,需要完全独立
-
状态管理: 在
Redux或Vuex中修改状态时 - 不可变更新: 函数式编程中创建新状态
- 数据隔离: 防止原始数据被意外修改
- 缓存数据: 保存数据快照
// 深拷贝适用场景 - Redux reducer
function todoReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
// 需要深拷贝来创建新状态
return {
...state,
todos: [
...state.todos,
{
id: action.id,
text: action.text,
completed: false
}
]
};
case 'TOGGLE_TODO':
// 深度更新嵌套对象
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
)
};
default:
return state;
}
}
5.3 性能优化策略
- 按需拷贝: 只拷贝需要修改的部分
-
结构共享: 使用不可变数据结构库(如
Immutable.js) - 增量更新: 只更新变化的部分
- 缓存结果: 对于相同输入返回缓存的结果
// 按需拷贝示例
function updateUser(user, updates) {
// 只深拷贝需要修改的部分
const updatedUser = { ...user };
if (updates.address) {
updatedUser.address = { ...user.address, ...updates.address };
}
if (updates.preferences) {
updatedUser.preferences = { ...user.preferences, ...updates.preferences };
}
// 其他属性直接浅拷贝
Object.keys(updates).forEach(key => {
if (key !== 'address' && key !== 'preferences') {
updatedUser[key] = updates[key];
}
});
return updatedUser;
}
// 使用代理实现惰性拷贝
function createLazyCopy(original) {
const changes = new Map();
const handler = {
get(target, prop) {
// 如果该属性有修改,返回修改后的值
if (changes.has(prop)) {
return changes.get(prop);
}
// 否则返回原始值
const value = target[prop];
// 如果是对象,则返回代理
if (value && typeof value === 'object') {
return new Proxy(value, handler);
}
return value;
},
set(target, prop, value) {
// 记录修改
changes.set(prop, value);
return true;
}
};
return new Proxy(original, handler);
}
5.4 安全性考虑
-
避免原型污染: 确保不会拷贝
__proto__等特殊属性 -
防止恶意对象: 处理具有
getter的执行可能引发副作用的对象 - 内存安全: 避免拷贝会导致内存泄漏的大型对象
// 安全的深拷贝实现
function safeDeepClone(obj, options = {}) {
const {
maxDepth = 100,
maxSize = 10000,
allowFunctions = false,
allowSymbols = true
} = options;
let size = 0;
function clone(current, depth, cache) {
// 检查深度限制
if (depth > maxDepth) {
throw new Error('Maximum depth exceeded');
}
// 检查大小限制
if (size > maxSize) {
throw new Error('Maximum size exceeded');
}
// 基本类型处理
if (current === null || typeof current !== 'object') {
// 检查 Symbol
if (typeof current === 'symbol' && !allowSymbols) {
throw new Error('Symbols are not allowed');
}
return current;
}
// 检查函数
if (typeof current === 'function') {
if (!allowFunctions) {
throw new Error('Functions are not allowed');
}
return current;
}
// 检查缓存(防止循环引用)
if (cache.has(current)) {
return cache.get(current);
}
// 根据类型创建空对象
let clone;
const Ctor = current.constructor;
switch (Ctor) {
case Date:
clone = new Date(current);
break;
case RegExp:
clone = new RegExp(current);
break;
case Map:
clone = new Map();
break;
case Set:
clone = new Set();
break;
case Array:
clone = [];
break;
default:
// 普通对象 - 避免原型污染
clone = Object.create(null);
}
// 存入缓存
cache.set(current, clone);
// 递归拷贝
if (current instanceof Map) {
current.forEach((value, key) => {
size++;
clone.set(
clone(key, depth + 1, cache),
clone(value, depth + 1, cache)
);
});
} else if (current instanceof Set) {
current.forEach(value => {
size++;
clone.add(clone(value, depth + 1, cache));
});
} else if (Array.isArray(current)) {
for (let i = 0; i < current.length; i++) {
size++;
clone[i] = clone(current[i], depth + 1, cache);
}
} else {
for (const key in current) {
// 避免拷贝原型链上的属性
if (current.hasOwnProperty(key)) {
// 避免特殊属性
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
size++;
clone[key] = clone(current[key], depth + 1, cache);
}
}
}
return clone;
}
return clone(obj, 0, new WeakMap());
}
六、现在JavaScript中的拷贝模式
6.1 不可变数据模式
// 使用 Object.freeze 实现浅不可变
const immutableConfig = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
headers: Object.freeze({
'Content-Type': 'application/json'
})
});
// 深度冻结
function deepFreeze(obj) {
Object.freeze(obj);
Object.getOwnPropertyNames(obj).forEach(prop => {
const value = obj[prop];
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
deepFreeze(value);
}
});
return obj;
}
// 使用 Proxy 实现不可变接口
function createImmutable(obj) {
const handler = {
get(target, prop) {
const value = target[prop];
// 如果是对象,返回代理
if (value && typeof value === 'object') {
return createImmutable(value);
}
return value;
},
set() {
throw new Error('Cannot modify immutable object');
},
deleteProperty() {
throw new Error('Cannot delete property from immutable object');
}
};
return new Proxy(obj, handler);
}
6.2 结构共享(Persistent Data Structures)
// 简化的结构共享实现
class PersistentMap {
constructor(data = {}) {
this.data = data;
this.version = 0;
}
set(key, value) {
// 创建新版本,共享未修改的数据
const newData = { ...this.data };
newData[key] = value;
const newMap = new PersistentMap(newData);
newMap.version = this.version + 1;
return newMap;
}
get(key) {
return this.data[key];
}
// 比较两个版本是否相等
equals(other) {
if (this === other) return true;
if (this.version !== other.version) return false;
// 深度比较(简化版)
return JSON.stringify(this.data) === JSON.stringify(other.data);
}
}
// 使用示例
const map1 = new PersistentMap({ a: 1, b: 2 });
const map2 = map1.set('c', 3);
const map3 = map2.set('b', 20);
console.log(map1.get('b')); // 2
console.log(map3.get('b')); // 20
console.log(map1.data === map2.data); // false
6.3 使用现代API
// 使用 Object.groupBy 和 Map (ES2024)
const users = [
{ id: 1, name: 'Alice', group: 'admin' },
{ id: 2, name: 'Bob', group: 'user' },
{ id: 3, name: 'Charlie', group: 'admin' }
];
// 分组并创建不可变结构
const grouped = Object.groupBy(users, user => user.group);
// 转换为不可变 Map
const immutableGroups = new Map(Object.entries(grouped));
// 深度冻结
function deepFreezeMap(map) {
map.forEach(value => {
if (value && typeof value === 'object') {
deepFreeze(value);
}
});
Object.freeze(map);
}
deepFreezeMap(immutableGroups);
// 创建新版本
const updatedGroups = new Map(immutableGroups);
updatedGroups.set('moderator', [{ id: 4, name: 'David', group: 'moderator' }]);
七、总结与最佳实践建议
7.1 拷贝方法选择指南
| 场景 | 推荐方法 | 理由 |
|---|---|---|
| 简单对象,无嵌套 | 浅拷贝({...obj})
|
快速、简单、高效 |
| 配置对象,少量嵌套 | 浅拷贝+手动处理嵌套 | 平衡性能和正确性 |
| 复杂嵌套对象 | 深拷贝(递归或sutrcturedClone) |
确保完全独立 |
| 包含特殊类型(Date、RegExp) | 自定义深拷贝或Lodash | 正确处理特殊对象 |
| 性能关键路径 | 按需拷贝+结构共享 | 最大化性能 |
| 不可变数据 | 深拷贝+Object.freeze
|
确保数据不可变 |
| 生产环境 | Lodash的_.cloneDeep
|
成熟、稳定、功能全 |
7.2 黄金法则
- 明确需求: 先确定是否需要深拷贝,很多时候浅拷贝就足够了
- 测试边界情况: 总是测试循环引用、特殊对象和大型数据结构
- 考虑性能: 对于频繁操作的数据,考虑使用不可变数据结构
- 保持简洁: 避免过度复杂的拷贝逻辑,必要时使用成熟的库
- 安全性第一: 处理用户输入时要特别小心,避免原型污染和其他安全问题
7.3 未来趋势
-
结构化克隆 API:
structuredClone()将成为深拷贝的标准方式 - Records和Tuples: ES提案,提供原生不可变数据结构
- 更快的拷贝算法: WebAssembly和新的 JavaScript 引擎优化
- 编译时优化: 通过静态分析优化拷贝操作
7.4 最终建议代码
// 生产环境推荐的拷贝工具函数
class CloneUtils {
// 简单的深拷贝(适合大多数场景)
static deepClone(obj) {
// 优先使用原生 API
if (typeof structuredClone === 'function') {
try {
return structuredClone(obj);
} catch (e) {
// 如果失败,回退到其他方法
}
}
// 回退到 JSON 方法(有限制)
try {
return JSON.parse(JSON.stringify(obj));
} catch (e) {
// 如果 JSON 方法失败,使用自定义实现
return this.customDeepClone(obj);
}
}
// 自定义深拷贝实现
static customDeepClone(obj, cache = new WeakMap()) {
// 基础类型和函数
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 检查缓存
if (cache.has(obj)) {
return cache.get(obj);
}
// 处理特殊对象
if (obj instanceof Date) {
const cloned = new Date(obj);
cache.set(obj, cloned);
return cloned;
}
if (obj instanceof RegExp) {
const cloned = new RegExp(obj);
cache.set(obj, cloned);
return cloned;
}
// 创建空对象/数组
const cloned = Array.isArray(obj) ? [] : {};
cache.set(obj, cloned);
// 递归拷贝属性
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = this.customDeepClone(obj[key], cache);
}
}
return cloned;
}
// 安全的浅拷贝(防止原型污染)
static safeShallowClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
const cloned = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key) &&
key !== '__proto__' &&
key !== 'constructor' &&
key !== 'prototype') {
cloned[key] = obj[key];
}
}
return cloned;
}
// 性能优化的拷贝(只拷贝需要修改的部分)
static smartClone(original, modifications) {
const result = { ...original };
for (const key in modifications) {
if (modifications.hasOwnProperty(key)) {
const originalValue = original[key];
const modifiedValue = modifications[key];
if (originalValue && typeof originalValue === 'object' &&
modifiedValue && typeof modifiedValue === 'object' &&
!Array.isArray(originalValue) && !Array.isArray(modifiedValue)) {
// 递归处理嵌套对象
result[key] = this.smartClone(originalValue, modifiedValue);
} else {
result[key] = modifiedValue;
}
}
}
return result;
}
}
// 使用示例
const data = {
user: {
name: 'John',
settings: {
theme: 'dark',
notifications: true
}
},
items: [1, 2, 3]
};
// 简单深拷贝
const cloned1 = CloneUtils.deepClone(data);
// 智能拷贝(只修改部分)
const cloned2 = CloneUtils.smartClone(data, {
user: {
settings: {
theme: 'light'
}
}
});
console.log(cloned2.user.settings.theme); // 'light'
console.log(cloned2.user.settings.notifications); // true(保持原值)
console.log(cloned2.items === data.items); // true(未修改的部分共享引用)
结语
JavaScript拷贝是一个看似简单实则复杂的话题。通过本文的学习,你应该能够:
- 理解浅拷贝和深拷贝的根本区别
- 根据不同的场景选择合适的拷贝策略
- 实现各种拷贝方法,处理边界情况
- 优化拷贝性能,避免常见陷阱 记住,没有一种拷贝方法是适用于所有场景的万能解决方案。最好的方法是理解每种技术的优缺点,根据具体需求做出明智的选择。
在实际开发中,当面临拷贝需求时,先问自己几个问题:
- 我真的需要完全独立的数据吗?
- 数据结构有多复杂?
- 性能要求有多高?
- 是否有特殊类型的对象需要处理?
通过回答这些问题,你将能够选择最合适的拷贝策略,写出更健壮、更高效的代码。
深入学习资源: