引言
在 JavaScript 中,赋值 、浅拷贝 和 深拷贝 是处理对象和数组时的三种常见操作。如果把 JavaScript 的数据操作比作文件管理,赋值就像创建快捷方式,浅拷贝类似复制文件夹结构,而深拷贝才是真正的文件克隆。三者各司其职,用错场景就会导致数据混乱!
一. 赋值:共享内存的引用
赋值是 JavaScript 中最基础的操作之一,但对于引用类型(对象、数组等)来说,赋值操作实际上只是复制了引用(内存地址),而不是创建新的独立数据。
赋值的本质
- 基本类型:赋值是值的拷贝(字符串、数字、布尔值等)
- 引用类型:赋值是引用的拷贝(对象、数组、函数等)
赋值的应用场景
赋值操作适用于:
- 需要多个变量指向同一对象时
- 函数参数传递(JavaScript 中参数是按值传递,但对于对象是传递引用的值)
示例
javascript
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = obj1; // 赋值
obj2.a = 10;
console.log(obj1.a); // 10(原对象被修改)
二. 浅拷贝(Shallow Copy):一级属性的独立副本
浅拷贝创建一个新对象,并复制原始对象的一级属性。如果属性是基本类型,则复制值;如果是引用类型,则复制引用。
特点
- 修改嵌套对象的属性会影响原对象。
- 适用于简单对象(没有嵌套对象或数组)。
浅拷贝的局限性
浅拷贝只能保证第一层属性的独立性,嵌套对象仍然是共享的。这在某些场景下会导致意外的数据污染。
实现方法
(1) 扩展运算符(...
)
javascript
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1 }; // 浅拷贝
obj2.b.c = 3;
console.log(obj1.b.c); // 3(原对象被修改)
(2) Object.assign
javascript
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1); // 浅拷贝
obj2.b.c = 3;
console.log(obj1.b.c); // 3(原对象被修改)
(3) 数组的 slice
或 concat
javascript
const arr1 = [1, 2, { a: 3 }];
const arr2 = arr1.slice(); // 浅拷贝
arr2[2].a = 4;
console.log(arr1[2].a); // 4(原数组被修改)
三. 深拷贝(Deep Copy):完全的独立副本
深拷贝创建一个全新的对象,并递归复制原始对象的所有属性,包括嵌套对象,使得新旧对象完全独立,互不影响。
特点
- 修改嵌套对象的属性不会影响原对象。
- 适用于复杂对象(包含嵌套对象或数组)。
实现方法
(1) JSON.parse(JSON.stringify(obj))
- 简单易用,但有以下限制:
- 不能复制函数、
undefined
、Symbol
。 - 不能处理循环引用。
- 不能复制函数、
javascript
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝
obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)
(2) 递归实现
- 手动实现深拷贝,支持所有数据类型。
javascript
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = deepClone(obj1); // 深拷贝
obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)
(3) 使用第三方库
- 使用 Lodash 的
cloneDeep
方法。
bash
npm install lodash
javascript
import _ from 'lodash';
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = _.cloneDeep(obj1); // 深拷贝
obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)
四、对比分析
特性 | 赋值 | 浅拷贝 | 深拷贝 |
---|---|---|---|
基本类型 | 值复制 | 值复制 | 值复制 |
引用类型 | 引用复制 | 一级属性值复制,嵌套属性引用复制 | 完全独立复制 |
内存占用 | 最小 | 中等 | 最大 |
性能 | 最快 | 较快 | 较慢 |
修改原对象影响 | 互相影响 | 一级属性不影响,嵌套属性影响 | 完全不影响 |
黄金定律:
- 超过 3 层嵌套考虑深拷贝
- 数据量 > 1MB 时慎用 JSON 法
- 循环结构必须用 WeakMap 方案
五、特殊情况的处理
5.1 循环引用
javascript
let obj = { a: 1 };
obj.self = obj;
// 简单的深拷贝会栈溢出
function cloneDeep(obj) {
const cloned = {};
for (let key in obj) {
if (typeof obj[key] === 'object') {
cloned[key] = cloneDeep(obj[key]);
} else {
cloned[key] = obj[key];
}
}
return cloned;
}
// 使用WeakMap解决循环引用
function cloneDeepWithCircular(obj, hash = new WeakMap()) {
if (hash.has(obj)) return hash.get(obj);
let cloned = Array.isArray(obj) ? [] : {};
hash.set(obj, cloned);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = typeof obj[key] === 'object' ?
cloneDeepWithCircular(obj[key], hash) : obj[key];
}
}
return cloned;
}
5.2 特殊对象处理
javascript
function cloneDeep(obj, hash = new WeakMap()) {
// 处理基本类型和null
if (obj === null || typeof obj !== 'object') return obj;
// 处理循环引用
if (hash.has(obj)) return hash.get(obj);
// 处理Date对象
if (obj instanceof Date) return new Date(obj);
// 处理RegExp对象
if (obj instanceof RegExp) return new RegExp(obj);
// 处理Set
if (obj instanceof Set) {
let clonedSet = new Set();
hash.set(obj, clonedSet);
obj.forEach(value => {
clonedSet.add(cloneDeep(value, hash));
});
return clonedSet;
}
// 处理Map
if (obj instanceof Map) {
let clonedMap = new Map();
hash.set(obj, clonedMap);
obj.forEach((value, key) => {
clonedMap.set(cloneDeep(key, hash), cloneDeep(value, hash));
});
return clonedMap;
}
// 处理普通对象和数组
let clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
// 处理Symbol属性
let symKeys = Object.getOwnPropertySymbols(obj);
if (symKeys.length) {
symKeys.forEach(symKey => {
clone[symKey] = cloneDeep(obj[symKey], hash);
});
}
// 处理普通属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = cloneDeep(obj[key], hash);
}
}
return clone;
}
六、现代JavaScript中的新特性
6.1 结构化克隆(Structured Clone)
HTML5 引入了结构化克隆算法,可用于 Worker 间通信或存储 API:
javascript
// 在浏览器环境中
let original = { a: 1, b: { c: 2 } };
let deepCopy = structuredClone(original);
// Node.js 中的类似功能
const v8 = require('v8');
let deepCopy = v8.deserialize(v8.serialize(original));
6.2 使用Proxy实现惰性拷贝
对于大型对象,可以结合 Proxy 实现按需深拷贝:
javascript
function createLazyDeepCopy(obj) {
const cache = new Map();
return new Proxy(obj, {
get(target, prop) {
const value = Reflect.get(target, prop);
if (typeof value === 'object' && value !== null) {
if (!cache.has(prop)) {
cache.set(prop, createLazyDeepCopy(value));
}
return cache.get(prop);
}
return value;
}
});
}
七、决策流程图:如何选择复制方式?
开始
↓
需要完全独立副本? → 是 → 深拷贝
↓否
需要共享数据变化? → 是 → 赋值
↓否
对象只有一层结构? → 是 → 浅拷贝
↓否
返回第一步重新思考需求
八、开发者的终极拷问
- 为什么
structuredClone
不能克隆函数?
→ 答:函数可能包含闭包等运行环境信息,如同不能复制一个人的记忆 - 深拷贝会复制原型链吗?
→ 答:大多数方案不会,如同复印文件不会复制打印机型号 - 如何判断该用哪种拷贝方式?
→ 记住口诀:"一变则变用赋值,浅层独立用浅拷,完全独立深拷贝"
九、课后彩蛋:console.log 的隐藏特性
javascript
const obj = { a: 1 };
console.log(obj); // 输出时可能显示修改后的值!
原理:控制台输出的是对象的实时引用,如同查看监控摄像头而非拍摄照片
十、总结:
复制三原则
- 经济原则:能赋值不拷贝,能浅拷不深拷
- 安全原则:处理循环引用和特殊对象就像拆炸弹
- 性能原则:深拷贝是最后的底牌,不是首选方案
总结对比表
特性 | 赋值 | 浅拷贝 | 深拷贝 |
---|---|---|---|
基本类型 | 复制值 | 同赋值 | 同赋值 |
引用类型 | 复制引用 | 新对象,复制一级属性 | 新对象,递归复制所有属性 |
嵌套对象影响 | 共享 | 共享嵌套对象 | 完全独立 |
性能 | 最优 | 较好 | 较差 |
实现方式 | = |
... , Object.assign() |
JSON方法 , _.cloneDeep() |
循环引用 | 天然支持 | 支持 | 需要特殊处理 |
函数/Symbol | 保留 | 保留 | JSON方法会丢失 |