序言:揭秘深浅拷贝,征服前端面试高频考点!
在前端开发的征途中,深拷贝与浅拷贝就像一对形影不离的“孪生兄弟”,也是面试官们最爱考察的经典题目之一。它们看似简单,却暗藏玄机,是理解 JavaScript 对象操作、内存管理和数据完整性的关键。混淆了它们,轻则代码出现隐蔽 Bug,重则引发数据安全风险。今天,就让我们一起深入剖析深浅拷贝的机制,掌握不同场景下的最佳实践,让你在面试中游刃有余,轻松过关!
一、浅拷贝利器:Object.assign() 详解
Object.assign() 是 JavaScript 中实现浅拷贝最常用的方法之一,以其简洁的语法和强大的合并能力深受开发者喜爱。
1. 核心用法:属性复制与合并
Object.assign(target, ...sources) 的核心功能是将一个或多个源对象(sources)的所有可枚举自有属性复制到目标对象(target)上,并返回修改后的目标对象。它遵循“后来居上”原则:同名属性会被后续源对象覆盖。
const target = { a: 1, b: 2 };
const source = { c: 3, d: 4 };
const result = Object.assign(target, source); // 将 source 的属性复制到 target
console.log(target); // { a: 1, b: 2, c: 3, d: 4 } (target 被修改)
console.log(source); // { c: 3, d: 4 } (source 不变)
console.log(result === target); // true (返回的就是被修改的 target 本身)
关键特性:
Object.assign()会直接修改传入的第一个参数(target)! 若想避免修改原对象,最佳实践是使用一个空对象{}作为目标:const newObj = Object.assign({}, obj1, obj2);。
2. 实战:属性冲突与覆盖
处理多个源对象且属性名冲突时,后传入对象的属性值将覆盖前面对象的属性值:
const configDefaults = { theme: 'light', fontSize: 14 };
const userSettings = { theme: 'dark', showNotifications: true };
const adminOverrides = { fontSize: 16 };
const finalConfig = Object.assign({}, configDefaults, userSettings, adminOverrides);
console.log(finalConfig); // { theme: 'dark', fontSize: 16, showNotifications: true }
3. 拷贝 Symbol 类型属性
Object.assign() 能够正确地复制 Symbol 类型的属性键:
const uniqueId = Symbol('userId');
const user = {
[uniqueId]: 12345,
name: 'Alice'
};
const userCopy = Object.assign({}, user);
console.log(userCopy); // { name: 'Alice', [Symbol(userId)]: 12345 } (Symbol 属性被成功复制)
4. 特殊处理:基本类型与 null/undefined
当源参数是基本类型(String, Number, Boolean)时,Object.assign() 会将其临时包装成对应的包装对象后再复制其可枚举的自有属性(主要是字符串的索引属性)。null 和 undefined 作为源参数则会被完全忽略。
const str = 'hello';
const num = 42;
const sym = Symbol('test');
const bool = true;
const result = Object.assign({}, str, null, num, undefined, bool, sym);
// 基本类型被包装,null/undefined 被跳过。
// 只有字符串包装对象有可枚举的自有属性(索引 0, 1, 2...)
console.log(result); // { 0: 'h', 1: 'e', 2: 'l', 3: 'l', 4: 'o' }
二、Object.assign() 的“阿喀琉斯之踵”:浅拷贝的局限性
虽然 Object.assign() 使用便捷,但其本质是浅拷贝(Shallow Copy) 。它只复制对象属性的第一层。如果属性值本身是对象(包括数组)或函数,那么拷贝的仅仅是该属性值在内存中的引用(地址) ,而非创建该值的一个新副本。
const original = {
id: 1,
profile: { // 嵌套对象
name: '小明',
hobbies: ['篮球', '羽毛球'] // 嵌套数组
}
};
// 浅拷贝 original
const shallowCopy = Object.assign({}, original);
// 修改浅拷贝对象的第一层属性 (id) - 不影响原对象
shallowCopy.id = 2;
console.log(original.id); // 1 (原对象未受影响)
// 修改浅拷贝对象的嵌套对象属性 (profile.name) - 影响原对象!
shallowCopy.profile.name = '小红';
console.log(original.profile.name); // '小红' (原对象也被修改了!)
// 修改浅拷贝对象的嵌套数组 (profile.hobbies) - 同样影响原对象!
shallowCopy.profile.hobbies.push('画画');
console.log(original.profile.hobbies); // ['篮球', '羽毛球', '画画'] (原对象也被修改了!)
问题根源: shallowCopy.profile 和 original.profile 指向内存中的同一个对象。对 shallowCopy.profile 的任何修改(如修改 name 或向 hobbies 数组 push),都会直接反映到 original.profile 上。
结论: 当对象结构层级较深(存在嵌套对象/数组) 时,使用 Object.assign() 进行浅拷贝存在数据意外共享的风险,可能导致难以追踪的 Bug 或敏感数据泄露。此时,我们需要寻求深拷贝(Deep Copy) 的解决方案。
三、深拷贝利器:JSON.parse(JSON.stringify())
对于需要完全隔离源对象和拷贝对象,特别是处理嵌套结构的场景,JSON.parse(JSON.stringify()) 是一种非常常用的深拷贝方法。
const original = {
id: 1,
profile: {
name: '小明',
hobbies: ['篮球', '羽毛球']
}
};
// 使用 JSON 方法进行深拷贝
const deepCopy = JSON.parse(JSON.stringify(original));
// 修改深拷贝对象的所有层级
deepCopy.id = 2;
deepCopy.profile.name = '小军';
deepCopy.profile.hobbies.push('画画');
console.log(deepCopy);
// 输出: { id: 2, profile: { name: '小军', hobbies: ['篮球', '羽毛球', '画画'] } }
console.log(original);
// 输出: { id: 1, profile: { name: '小明', hobbies: ['篮球', '羽毛球'] } } (原对象纹丝不动!)
核心原理:
JSON.stringify(source): 将 JavaScript 对象或值序列化(Serialization) 成一个 JSON 格式的字符串。这个过程会递归遍历对象的所有层级。JSON.parse(jsonString): 将 JSON 格式的字符串解析(Parsing) 成一个全新的 JavaScript 对象。新对象与原对象在内存中完全独立,没有任何引用关系。
重大局限:
JSON.parse(JSON.stringify()) 虽然能有效解决嵌套对象的深拷贝问题,但它不是万能的,存在以下无法拷贝或会丢失的情况:
函数(Function): 在序列化过程中会被完全忽略。undefined: 作为属性值时,在序列化过程中会被忽略。Symbol: 作为属性键或值,在序列化过程中都会被忽略。- 循环引用(Circular References) : 如果对象 A 的属性引用了对象 B,而对象 B 的属性又引用了对象 A,形成循环引用。
JSON.stringify()会直接报错(Uncaught TypeError: Converting circular structure to JSON)。 - 特殊对象: 如
Date对象会被转换成 ISO 格式字符串(解析后不再是Date实例,而是字符串),RegExp、Map、Set、Blob等类型通常会被序列化为空对象{}或直接丢失。 - 原型链(Prototype Chain) : 新对象的原型指向
Object.prototype,原对象的自定义原型链信息会丢失。
结论: JSON.parse(JSON.stringify()) 是处理纯数据对象(不包含函数、Symbol、undefined、循环引用和特殊内置对象) 深拷贝的快速有效方案。一旦涉及上述无法处理的类型或结构,就需要寻求其他深拷贝方法(如递归拷贝、使用第三方库 lodash 的 _.cloneDeep、或现代浏览器中的 structuredClone())。
四、拷贝江湖:不同场景下的兵器谱
掌握了深浅拷贝的核心原理和代表方法,我们就能根据实际场景,灵活选用最合适的“兵器”:
-
基础数据类型 (Primitive Values):
=赋值足矣-
String,Number,Boolean,null,undefined,Symbol,BigInt都是存储在栈内存中的基础类型。 -
使用
=赋值时,会直接复制值本身。新变量和原变量完全独立。
let price = 100; let discountedPrice = price; // 复制值 100 discountedPrice = 80; // 修改新变量 console.log(price); // 100 (原变量不变) console.log(discountedPrice); // 80 -
-
浅层对象/数组:便捷的浅拷贝方法
-
当对象/数组只有一层结构(没有嵌套对象/数组) ,或你明确只需要拷贝第一层且不介意共享内层引用时,可以使用以下浅拷贝方法:
-
Object.assign({}, obj): 复制对象的可枚举自有属性。 -
Array.prototype.slice(): 复制数组的一部分(默认复制整个数组)。 -
Array.prototype.concat([]): 合并数组(与空数组合并即复制)。 -
展开运算符
[...arr]/{ ...obj }: ES6 提供的简洁语法。
-
// 简单数组 - 浅拷贝有效 const fruits = ['apple', 'banana', 'cherry']; const fruitsCopy = fruits.slice(); // 或 [...fruits], fruits.concat() fruitsCopy[1] = 'orange'; console.log(fruits); // ['apple', 'banana', 'cherry'] (原数组不变) console.log(fruitsCopy); // ['apple', 'orange', 'cherry'] // 嵌套数组 - 浅拷贝的局限性 (第二层仍是引用) const matrix = [[1, 2], [3, 4], [5, [6, 7]]]; const matrixCopy = matrix.slice(); // 浅拷贝 matrixCopy[0][0] = 100; // 修改第一层数组里的嵌套数组元素 matrixCopy[2][1][0] = 600; // 修改更深层的元素 console.log(matrix); // [[100, 2], [3, 4], [5, [600, 7]]] (原数组被修改了!) console.log(matrixCopy); // [[100, 2], [3, 4], [5, [600, 7]]] -
-
纯数据对象的深拷贝:
JSON.parse(JSON.stringify())- 如前所述,适用于不包含函数、Symbol、undefined、循环引用和特殊内置对象的纯数据对象。优点是原生支持、简单直接。缺点是局限性明显。
五、总结与通关钥匙
深拷贝与浅拷贝的区分,核心在于是否创建了嵌套对象/数组的新副本,从而断开原对象与拷贝对象在内层数据上的引用连接。
- 浅拷贝 (
Object.assign,slice,concat,spread) : 只复制第一层属性。嵌套对象/数组是共享的引用。操作简单高效,适用于简单结构或明确共享内层的场景。警惕数据意外共享风险! - 深拷贝 (
JSON.parse(JSON.stringify),_.cloneDeep,structuredClone) : 递归复制所有层级属性。嵌套对象/数组也是全新的副本。源对象与拷贝对象完全隔离。适用于需要完全独立副本的场景。注意不同方法的局限性(函数、Symbol、循环引用等) 。
面试通关钥匙:
-
清晰定义: 能准确解释深拷贝与浅拷贝的根本区别(内存引用关系)。
-
掌握代表方法:
- 浅拷贝:熟练说出
Object.assign, 数组的slice/concat/展开运算符 及其行为。 - 深拷贝:重点掌握
JSON.parse(JSON.stringify())的原理、优势(纯数据深拷贝) 和 重大局限(函数、Symbol、循环引用等) 。了解递归实现或库方案(如lodash.cloneDeep)作为补充。知道现代structuredClone()API(兼容性需注意)。
- 浅拷贝:熟练说出
-
理解适用场景: 能根据数据结构(是否有嵌套?)和需求(是否需要完全隔离?)选择合适的拷贝方式。
-
警惕引用陷阱: 能识别浅拷贝导致的数据共享问题,并解释原因。
-
动手能力: 面试官可能会要求手写一个简单的深拷贝函数(处理基本类型、对象、数组,考虑循环引用是加分项)。
理解了这些核心要点,你就能在深浅拷贝的“拷”问中从容应对,轻松拿下这道面试高频题!加油,未来的前端大牛!