🧩 从无拷贝到拷贝:一个演进的故事
想象你有一本珍贵的笔记本📒,朋友想借阅时,如果你直接把原本给他(无拷贝),那么他做的任何修改都会直接影响你的原始笔记 - 这显然风险很大。于是你决定复印一份(浅拷贝),这时朋友可以在复印件上写写画画,但问题来了:如果你的笔记本里有可拆卸的便利贴📝,朋友把便利贴拿走或修改了,你的原笔记本里的便利贴也会受到影响!最后你找到了完美解决方案:不仅复印笔记本的每一页,还把里面所有的便利贴也一一复印(深拷贝),这样无论朋友怎么修改他的副本,你的原始笔记都能保持完好无损。这就是从无拷贝到浅拷贝再到深拷贝的演进过程!
什么是拷贝?🤔
在 JavaScript 中,拷贝就是创建一个与原始数据值相同但内存独立的新数据。让我们先看看不拷贝会发生什么:
javascript
let arr = [1, 2, 3];
let newArr = arr; // 这只是引用赋值,不是拷贝!
newArr[0] = 100;
console.log(arr); // [100, 2, 3] 😱
这里修改 newArr
影响了 arr
,因为它们指向同一个内存地址!
浅拷贝:只拷贝第一层 🏊♂️
浅拷贝会创建一个新对象,但只复制对象的第一层属性。如果属性是引用类型,则复制的是引用地址。
5种常用浅拷贝方法:
- 手动实现 👷♂️
javascript
function shallowClone(target) {
// 1. 处理非对象类型或null值(直接返回)
if (typeof target !== 'object' || target === null) return target;
// 2. 根据目标类型初始化克隆对象(数组或普通对象)
const clone = Array.isArray(target) ? [] : {};
// 3. 遍历目标对象的自有属性
for (let key in target) {
// 使用hasOwnProperty确保只拷贝对象自身的属性(不拷贝原型链上的属性)
if (target.hasOwnProperty(key)) {
clone[key] = target[key]; // 直接复制属性值(浅拷贝)
}
}
return clone;
}
hasOwnProperty()的作用:
- 是JavaScript对象的内置方法
- 用于检查对象是否自身拥有指定的属性(不包括从原型链继承的属性)
- 语法:
obj.hasOwnProperty(propertyName)
- 在拷贝时使用可以避免意外拷贝原型链上的属性(如Object.prototype上的方法)
javascript
const obj = { name: '小明', age: 18 };
const newObj = Object.assign({}, obj);
- 数组的 concat() ➕
javascript
const arr = [1, 2, 3];
const newArr = arr.concat();
- 数组的 slice() ✂️
javascript
const arr = [1, 2, 3];
const newArr = arr.slice();
- 展开运算符 ... 🌈
javascript
const arr = [1, 2, 3];
const newArr = [...arr];
const obj = { a: 1, b: 2 };
const newObj = { ...obj };
浅拷贝的局限性 ⚠️
javascript
const person = { name: '小明', hobbies: ['篮球', '游泳'] };
const clonePerson = shallowClone(person);
clonePerson.hobbies.push('跑步');
console.log(person.hobbies); // ['篮球', '游泳', '跑步'] 😅
虽然 person
和 clonePerson
是两个对象,但它们共享同一个 hobbies
数组引用!
深拷贝:彻底的内存分离 🏗️
深拷贝会递归复制所有层级,创建一个完全独立的新对象。
简易深拷贝方法(有局限)🚧
javascript
const cloned = JSON.parse(JSON.stringify(obj));
⚠️ 这个方法的问题:
-
❌无法处理循环引用(会栈溢出)
javascriptconst a = {val:2}; a.target = a;
拷贝a会出现系统栈溢出,因为出现了
无限递归
的情况。 -
❌ 会丢失特殊对象(Date、RegExp等)
-
❌ 无法复制函数
-
❌ 会忽略Symbol类型的键
完整版深拷贝实现 🛠️
javascript
/**
* 深度克隆任意JavaScript对象,处理循环引用和各种内置类型
* @param {*} target - 需要克隆的目标值,可以是任意类型
* @param {WeakMap} [map=new WeakMap()] - 用于跟踪已克隆对象的WeakMap,解决循环引用问题
* @returns {*} 返回深度克隆后的副本
* @throws {Error} 对于明确不支持克隆的类型可能会抛出错误(根据配置)
*/
function deepClone(target, map = new WeakMap()) {
// 基本类型直接返回(包括:string, number, boolean, null, undefined, symbol, bigint)
// 函数也直接返回(函数通常不需要也不应该被深拷贝)
if (typeof target !== 'object' || target === null) {
return target;
}
// 处理循环引用:如果已经克隆过该对象,则直接返回之前克隆的结果
// 这是解决循环引用的关键,避免无限递归
if (map.has(target)) {
return map.get(target);
}
// 使用Object.prototype.toString方法获取精确的对象类型
// 比typeof和instanceof更可靠,能区分各种内置对象类型
const type = Object.prototype.toString.call(target);
// 根据不同类型采用不同的克隆策略
switch (type) {
case '[object Date]':
// Date对象:通过时间戳创建新实例
// 确保克隆后的对象与原始对象时间相同但独立
return new Date(target.getTime());
case '[object RegExp]': {
// 正则表达式处理
const regFlags = /\w*$/; // 匹配标志的正则表达式(g, i, m等)
// 创建新正则,复制源表达式和标志
const result = new RegExp(target.source, regFlags.exec(target));
// 复制lastIndex属性(记录正则匹配位置状态)
result.lastIndex = target.lastIndex;
return result;
}
case '[object Map]': {
// Map处理
const result = new Map();
// 先存入WeakMap,再填充内容,解决循环引用
map.set(target, result);
// 遍历原始Map,递归克隆key和value
target.forEach((val, key) => {
result.set(deepClone(key, map), deepClone(val, map));
});
return result;
}
case '[object Set]': {
// Set处理
const result = new Set();
map.set(target, result);
// 遍历原始Set,递归克隆每个值
target.forEach(val => {
result.add(deepClone(val, map));
});
return result;
}
case '[object Array]': {
// 数组处理
const result = [];
map.set(target, result);
// 遍历数组,递归克隆每个元素
target.forEach((item, index) => {
result[index] = deepClone(item, map);
});
return result;
}
case '[object Object]': {
// 普通对象处理(包括自定义类实例)
// 获取对象的所有自身属性(包括Symbol属性)
const keys = Reflect.ownKeys(target);
const result = {};
map.set(target, result);
// 遍历所有属性
keys.forEach(key => {
// 只克隆可枚举属性(与JSON.stringify行为一致)
if (target.propertyIsEnumerable(key)) {
result[key] = deepClone(target[key], map);
}
});
// 保持原型链:将克隆对象的原型设置为原对象的原型
// 这使得克隆后的自定义类实例仍然能访问原型方法
Object.setPrototypeOf(result, Object.getPrototypeOf(target));
return result;
}
default:
// 对于其他未处理的特殊对象(如Error, Promise, WeakMap, WeakSet等)
// 返回原对象是最安全的做法,因为这些对象:
// 1. 可能有特殊内部状态无法克隆
// 2. 克隆后可能破坏其正常功能
// 也可以选择抛出错误让调用者明确知道不支持
return target;
// 或者: throw new Error(`Unsupported type: ${type}`);
}
}
使用示例 🌟
javascript
const original = {
name: '小明',
age: 18,
hobbies: ['篮球', '游泳'],
birth: new Date('2000-01-01'),
friends: new Set(['小红', '小刚']),
metadata: new Map([['id', 123]]),
sayHi: function() { console.log('Hi!'); }
};
// 添加循环引用
original.self = original;
const cloned = deepClone(original);
console.log(cloned !== original); // true ✅
console.log(cloned.hobbies !== original.hobbies); // true ✅
console.log(cloned.self === cloned); // true ✅ 循环引用正确处理
总结 📚
特性 | 浅拷贝 🏊♂️ | 深拷贝 🏗️ |
---|---|---|
拷贝层级 | 仅第一层 | 所有层级 |
引用类型处理 | 共享引用 | 完全独立 |
性能 | 快 ⚡ | 慢 🐢 |
适用场景 | 简单对象 | 复杂嵌套对象 |
选择拷贝方式时,要根据实际需求权衡:
- 如果确定对象结构简单,使用浅拷贝性能更好
- 如果对象有嵌套结构或需要完全隔离,使用深拷贝更安全
希望这篇博客能帮助你理解 JavaScript 中的拷贝机制!🎉 如果有任何问题,欢迎留言讨论~ 💬