在前端开发内容学习中,手写深拷贝:从 JSON 到递归,彻底解决循环引用问题是常见主题。很多人在阅读时会遇到概念分散、步骤不清和注意点难以归纳的问题。本文按照基础概念、操作流程和关键细节,对相关内容进行整理。
在前端开发中,对象拷贝是一个高频话题。面试时经常被问到:

"请手写一个深拷贝,并处理循环引用的情况"
今天我们从浅拷贝的问题出发,逐步实现一个完整的深拷贝函数,彻底搞懂这个问题。
// 1.js
const users = [
{ id: 1, name: "莫某", hometown: "南昌" },
{ id: 2, name: "张三", hometown: "南昌" },
{ id: 3, name: "李四", hometown: "南昌" }
]
// 引用式拷贝:data 和 users 指向同一个堆内存地址
const data = users;
// 修改 data,users 也会被修改
data[0].hobbies = ["篮球", "看烟花"];
console.log(data, users);
// 两个数组都包含了 hobbies 属性!
逐行解析:
const users = [...]
users 是一个数组,存储在堆内存中const data = users;
data 和 users 指向同一个堆内存地址data 就是修改 usersdata[0].hobbies = ["篮球", "看烟花"];
data[0],实际上修改的是堆内存中的对象users[0] 也会被修改,因为它们指向同一个对象内存模型:
栈内存 堆内存
┌─────────┐ ┌─────────────────┐
│ users │────>│ [{id:1,...}, │
└─────────┘ │ {id:2,...}, │
┌─────────┐ │ {id:3,...}] │
│ data │────>│ (同一个地址) │
└─────────┘ └─────────────────┘
浅拷贝只拷贝第一层,嵌套对象仍然是引用:
// 浅拷贝实现
function shallowClone(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const clone = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = obj[key]; // 只拷贝第一层
}
}
return clone;
}
// 测试
const original = {
name: '莫某',
address: { city: '南昌' }
};
const copy = shallowClone(original);
copy.name = '张三'; // 不影响 original
copy.address.city = '北京'; // original.address.city 也变成 '北京'!
console.log(original.address.city); // '北京'
浅拷贝的局限:
深拷贝需要:
// 2.js
var users = [
{ id: 1, name: "莫某", hometown: "南昌" },
{ id: 2, name: "张三", hometown: "南昌" },
{ id: 3, name: "李四", hometown: "南昌" }
];
// JSON 序列化 + 反序列化
var data = JSON.parse(JSON.stringify(users));
data[0].hobbies = ["篮球", "看烟花"];
console.log(data, users);
// data 有 hobbies,users 没有!
逐行解析:
JSON.stringify(users)
users 序列化成 JSON 字符串JSON.parse(...)
var data = JSON.parse(JSON.stringify(users));
data 和 users 指向不同的堆内存地址data 不会影响 users缺陷 1:无法处理函数
const obj = {
name: '莫某',
sayHello: function() {
console.log('Hello');
}
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.sayHello); // undefined,函数丢失了!
缺陷 2:无法处理 undefined
const obj = {
name: '莫某',
age: undefined
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.age); // undefined,但属性不存在!
console.log('age' in copy); // false
缺陷 3:无法处理 Symbol
const obj = {
name: '莫某',
[Symbol('id')]: 123
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(Object.getOwnPropertySymbols(copy)); // [],Symbol 丢失!
const obj = { name: '莫某' };
obj.self = obj; // 循环引用
const copy = JSON.parse(JSON.stringify(obj));
// TypeError: Converting circular structure to JSON
缺陷 5:Date 变成字符串
const obj = {
name: '莫某',
birthday: new Date('1990-01-01')
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.birthday); // "1990-01-01T00:00:00.000Z"(字符串)
console.log(copy.birthday instanceof Date); // false
缺陷 6:RegExp 变成空对象
const obj = {
name: '莫某',
pattern: /hello/gi
};
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.pattern); // {},正则表达式丢失!
JSON 方案对比:
function deepClone(obj) {
// 1. 处理基本类型
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 2. 创建空对象或数组
const clone = Array.isArray(obj) ? [] : {};
// 3. 遍历所有属性
for (let key in obj) {
// 只处理自身属性
if (obj.hasOwnProperty(key)) {
// 递归拷贝
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
// 测试
const original = {
name: '莫某',
address: { city: '南昌', zip: '330000' },
hobbies: ['篮球', '看烟花']
};
const copy = deepClone(original);
copy.address.city = '北京';
copy.hobbies.push('编程');
console.log(original.address.city); // '南昌'
console.log(original.hobbies); // ['篮球', '看烟花']
逐行解析:
if (typeof obj !== 'object' || obj === null) {
return obj;
}
null 也直接返回const clone = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
for...in 遍历所有可枚举属性hasOwnProperty 过滤掉原型链上的属性deepClone,处理嵌套对象function deepClone(obj) {
// 1. 处理基本类型
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 2. 处理 Date
if (obj instanceof Date) {
return new Date(obj);
}
// 3. 处理 RegExp
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 4. 处理数组和对象
const clone = Array.isArray(obj) ? [] : {};
// 5. 处理 Symbol 键
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (let key of symbolKeys) {
clone[key] = deepClone(obj[key]);
}
// 6. 处理普通属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
// 测试
const original = {
name: '洪强',
birthday: new Date('1990-01-01'),
pattern: /hello/gi,
[Symbol('id')]: 123
};
const copy = deepClone(original);
console.log(copy.birthday instanceof Date); // true
console.log(copy.pattern instanceof RegExp); // true
console.log(Object.getOwnPropertySymbols(copy)); // [Symbol(id)]
逐行解析:
if (obj instanceof Date) {
return new Date(obj);
}
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
obj.source 是正则表达式的模式obj.flags 是标志(g/i/m 等)const symbolKeys = Object.getOwnPropertySymbols(obj);
for (let key of symbolKeys) {
clone[key] = deepClone(obj[key]);
}
Object.getOwnPropertySymbols 获取所有 Symbol 键for...in 遍历到,需要单独处理const obj = { name: '莫某' };
obj.self = obj; // 对象引用自己
deepClone(obj);
// RangeError: Maximum call stack size exceeded
解决方案:使用 WeakMap 记录已拷贝的对象
function deepClone(obj, hash = new WeakMap()) {
// 1. 处理基本类型
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 2. 检查是否已经拷贝过
if (hash.has(obj)) {
return hash.get(obj); // 返回已拷贝的引用
}
// 3. 处理特殊类型
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
// 4. 创建新对象
const clone = Array.isArray(obj) ? [] : {};
// 5. 记录到 hash 中
hash.set(obj, clone);
// 6. 处理 Symbol 键
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (let key of symbolKeys) {
clone[key] = deepClone(obj[key], hash);
}
// 7. 处理普通属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
return clone;
}
// 测试循环引用
const obj = { name: '莫某' };
obj.self = obj;
const copy = deepClone(obj);
console.log(copy.self === copy); // true,循环引用被正确处理
console.log(copy.self.name); // '洪强'
逐行解析:
function deepClone(obj, hash = new WeakMap()) {
hash,默认值是新的 WeakMaphash 用于记录已经拷贝过的对象if (hash.has(obj)) {
return hash.get(obj);
}
obj 是否已经拷贝过const clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
hash 中clone[key] = deepClone(obj[key], hash);
hash 参数hashWeakMap vs Map:
为什么用 WeakMap?
function deepClone(obj, hash = new WeakMap()) {
// 1. 处理基本类型和 null
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 2. 处理循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
// 3. 处理 Date
if (obj instanceof Date) {
return new Date(obj);
}
// 4. 处理 RegExp
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 5. 处理 Map
if (obj instanceof Map) {
const clone = new Map();
hash.set(obj, clone);
obj.forEach((value, key) => {
clone.set(deepClone(key, hash), deepClone(value, hash));
});
return clone;
}
// 6. 处理 Set
if (obj instanceof Set) {
const clone = new Set();
hash.set(obj, clone);
obj.forEach(value => {
clone.add(deepClone(value, hash));
});
return clone;
}
// 7. 处理函数(可选:直接返回或拷贝)
if (typeof obj === 'function') {
return obj; // 或者使用 eval('(' + obj.toString() + ')')
}
// 8. 创建数组或对象
const clone = Array.isArray(obj) ? [] : {};
// 9. 记录到 hash
hash.set(obj, clone);
// 10. 处理 Symbol 键
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (let key of symbolKeys) {
clone[key] = deepClone(obj[key], hash);
}
// 11. 处理普通属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
return clone;
}
逐行解析:
if (obj instanceof Map) {
const clone = new Map();
hash.set(obj, clone);
obj.forEach((value, key) => {
clone.set(deepClone(key, hash), deepClone(value, hash));
});
return clone;
}
hash,再处理内容,防止循环引用if (obj instanceof Set) {
const clone = new Set();
hash.set(obj, clone);
obj.forEach(value => {
clone.add(deepClone(value, hash));
});
return clone;
}
if (typeof obj === 'function') {
return obj;
}
eval 或 new Functionconst toString = Object.prototype.toString;
function getType(obj) {
return toString.call(obj);
}
function deepClone(obj, hash = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) return obj;
if (hash.has(obj)) return hash.get(obj);
const type = getType(obj);
if (type === '[object Date]') return new Date(obj);
if (type === '[object RegExp]') return new RegExp(obj.source, obj.flags);
if (type === '[object Map]') { /* ... */ }
if (type === '[object Set]') { /* ... */ }
// ...
}
优化点:
Object.prototype.toString 获取准确类型instanceof 判断function deepClone(obj, hash = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) return obj;
if (hash.has(obj)) return hash.get(obj);
// 获取原型
const proto = Object.getPrototypeOf(obj);
// 创建新对象,保持原型链
const clone = Object.create(proto);
hash.set(obj, clone);
// 获取所有属性(包括不可枚举的)
const descriptors = Object.getOwnPropertyDescriptors(obj);
for (let key in descriptors) {
const descriptor = descriptors[key];
if (typeof descriptor.value === 'object' && descriptor.value !== null) {
descriptor.value = deepClone(descriptor.value, hash);
}
Object.defineProperty(clone, key, descriptor);
}
return clone;
}
优化点:
Object.create(proto) 保持原型链Object.getOwnPropertyDescriptors 获取所有属性描述符Object.defineProperty 保持属性的可枚举性、可配置性等性能对比:
const obj = {
name: '莫某',
address: { city: '南昌', zip: '330000' },
hobbies: ['篮球', '看烟花']
};
// JSON 方案:~0.05ms
console.time('JSON');
JSON.parse(JSON.stringify(obj));
console.timeEnd('JSON');
// 递归方案:~0.1ms
console.time('递归');
deepClone(obj);
console.timeEnd('递归');
Q1:为什么要用 WeakMap?
Q2:如何处理函数?
eval 或 new FunctionQ3:如何处理原型链?
Object.getPrototypeOf 获取原型Object.create 创建保持原型链的新对象Object.getOwnPropertyDescriptors 获取所有属性Q4:如何优化性能?
function deepClone(obj, hash = new WeakMap()) {
// 1. 基本类型
if (typeof obj !== 'object' || obj === null) return obj;
// 2. 循环引用
if (hash.has(obj)) return hash.get(obj);
// 3. 特殊类型
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
// 4. 创建新对象
const clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
// 5. 递归拷贝
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
// 6. Symbol 键
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (let key of symbolKeys) {
clone[key] = deepClone(obj[key], hash);
}
return clone;
}
1. 处理基本类型 → 直接返回
2. 处理循环引用 → 使用 WeakMap 记录
3. 处理特殊类型 → Date、RegExp、Map、Set
4. 创建新对象 → 数组或对象
5. 递归拷贝 → 遍历所有属性
6. 处理 Symbol → 单独遍历 Symbol 键