深浅拷贝是前端面试的 "常驻嘉宾"------ 它不仅考察你对 JS 内存模型的理解,还能延伸到手写代码、性能优化等深层能力。很多面试官会从Object.assign()切入,一步步挖掘你的知识边界。这篇文章就按面试答题逻辑,把深浅拷贝的核心考点讲清楚,帮你在 "表演时间" 稳拿分。
一、浅拷贝:从 Object.assign () 说起
浅拷贝的核心是 "只拷贝对象表层属性,深层属性仍共享引用"------ 简单说,修改拷贝后对象的深层属性,会影响原对象。而Object.assign()是浅拷贝的典型代表,也是面试高频考点。
1.1 Object.assign () 的核心特性(面试必答)
先纠正一个常见误区:Object.assign() 会返回修改后的目标对象。如果目标对象是新创建的空对象(如{}),那本质上就是生成了一个 "新的浅拷贝对象"。
它的完整逻辑是:
- 作用 :将 1 个或多个 "源对象" 的可枚举属性(enumerable: true)复制到 "目标对象"。
- 返回值:修改后的 "目标对象"(不是全新对象,除非目标对象本身是新创建的)。
- 浅拷贝本质 :若源对象的属性值是 "复杂数据类型"(对象 / 数组),拷贝的是其引用地址,而非实际值。
举个例子,一看就懂:
ini
const obj = {
name: "hxt",
info: { age: 24 } // 深层复杂属性
};
// 目标对象是新创建的空对象,返回新的浅拷贝对象
const copyObj = Object.assign({}, obj);
copyObj.name = "newHxt"; // 修改表层属性:不影响原对象
copyObj.info.age = 25; // 修改深层属性:影响原对象
console.log(obj.name); // "hxt"(表层不变)
console.log(obj.info.age); // 25(深层被修改)
1.2 其他常见浅拷贝方法
除了Object.assign(),日常开发中还有这些浅拷贝方式,面试时提一句能体现知识面:
- 数组专用:slice()、concat()(本质是返回新数组,但深层数组仍共享引用)
ini
const arr = [1, [2, 3]];
const copyArr = arr.slice(); // 浅拷贝数组
copyArr[1][0] = 20;
console.log(arr[1][0]); // 20(深层被修改)
- 扩展运算符(...) :对对象 / 数组通用,语法更简洁,同样是浅拷贝
ini
const copyObj = { ...obj }; // 对象浅拷贝
const copyArr = [...arr]; // 数组浅拷贝
1.3 浅拷贝的使用场景
面试时被问 "什么时候用浅拷贝?",可以这样答:
当对象 / 数组的深层结构不需要修改,只需要复制表层属性时(比如合并配置、创建简单副本),用浅拷贝足够 ------ 比深拷贝更高效,避免不必要的性能开销。
二、深拷贝:JSON 方法的便捷与局限
深拷贝的核心是 "完全切断引用,拷贝所有层级的属性"------ 修改拷贝对象的任何属性,都不会影响原对象。最常用的便捷方法是JSON.parse(JSON.stringify()),但它的局限是面试重点。
2.1 JSON 深拷贝的原理与优势
它的逻辑很简单:
- JSON.stringify(obj):将对象 "序列化" 为 JSON 字符串(把 JS 数据类型转成字符串格式);
- JSON.parse(str):将 JSON 字符串 "反序列化" 为新对象(重新创建内存空间,生成全新对象)。
优势是无需手写代码,一行实现深拷贝,适合简单场景:
ini
const obj = {
field1: 1,
field4: { child: "child" }
};
const deepCopyObj = JSON.parse(JSON.stringify(obj));
deepCopyObj.field4.child = "newChild";
console.log(obj.field4.child); // "child"(完全不影响原对象)
2.2 致命局限:这些情况它搞不定(面试必讲)
JSON.stringify()有严格的 "序列化规则",导致它无法处理以下场景,这也是面试官会追问的点:
- 1. 无法拷贝 undefined、function、Symbol:这三类值不是合法的 JSON 数据类型,会被直接忽略(对象属性)或转为null(数组元素);
- 2. 无法处理循环引用:若对象自身引用自身(如obj.obj = obj),序列化时会直接报错(TypeError: Converting circular structure to JSON);
- 3. 特殊类型失真:Date 会被转成字符串、RegExp 会被转成空对象,无法保留原类型特性。
看个例子直观感受:
javascript
const obj = {
func: () => {}, // function:被忽略
undef: undefined, // undefined:被忽略
sym: Symbol("test"), // Symbol:被忽略
date: new Date() // Date:被转成字符串
};
console.log(JSON.parse(JSON.stringify(obj)));
// 输出:{ date: "2024-05-XXTXX:XX:XX.XXXZ" }(其他属性全丢了)
三、手写深拷贝:从基础到解决循环引用
当JSON方法满足不了需求时,就需要手写深拷贝 ------ 这是面试的 "加分项",能体现你的逻辑能力。我们分两步实现:先写基础版,再解决循环引用问题。
3.1 基础版深拷贝:递归遍历
核心思路:判断数据类型,简单类型直接返回,复杂类型(对象 / 数组)递归拷贝每一层属性。
代码实现:
javascript
function clone(target) {
// 1. 简单类型(Number/String/Boolean/null/undefined/Symbol/function)直接返回
if (typeof target !== "object" || target === null) {
return target;
}
// 2. 复杂类型:区分数组和对象
let cloneTarget = Array.isArray(target) ? [] : {};
// 3. 遍历属性,递归拷贝每一层
for (const key in target) {
// 只拷贝对象自身的属性(排除原型链上的属性)
if (target.hasOwnProperty(key)) {
cloneTarget[key] = clone(target[key]);
}
}
return cloneTarget;
}
问题暴露:如果遇到循环引用(如obj.obj = obj),递归会无限执行,最终导致栈溢出(RangeError: Maximum call stack size exceeded)。
3.2 优化版:用 WeakMap 解决循环引用
要解决循环引用,关键是缓存已拷贝的对象------ 当再次遇到相同对象时,直接返回缓存的结果,避免重复递归。
这里为什么用WeakMap而不是Map?面试必答!
- Map的键是 "强引用":即使键对应的对象在外部被销毁,Map仍会持有引用,导致内存泄漏;
- WeakMap的键是 "弱引用":当键对应的对象在外部没有其他引用时,会被垃圾回收机制回收,不会造成内存泄漏。
优化后的代码:
javascript
function deepClone(target, cache = new WeakMap()) {
// 1. 简单类型直接返回
if (typeof target !== "object" || target === null) {
return target;
}
// 2. 检查缓存:若已拷贝过该对象,直接返回缓存结果(解决循环引用)
if (cache.has(target)) {
return cache.get(target);
}
// 3. 区分数组和对象,创建拷贝容器
let cloneTarget = Array.isArray(target) ? [] : {};
// 4. 将当前对象存入缓存(键:原对象,值:拷贝对象)
cache.set(target, cloneTarget);
// 5. 递归拷贝属性(处理对象)
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = deepClone(target[key], cache);
}
}
// 处理Date/RegExp等特殊类型(面试若没要求可省略)
if (target instanceof Date) {
return new Date(target);
}
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
}
return cloneTarget;
}
测试循环引用场景:
ini
const obj = { name: "hxt" };
obj.self = obj; // 循环引用:对象引用自身
const copyObj = deepClone(obj);
console.log(copyObj.self === copyObj); // true(拷贝成功,无栈溢出)
四、面试答题思路总结
当面试官问 "谈谈你对深浅拷贝的理解" 时,别零散地回答,按这个框架说,逻辑清晰又全面:
- 先定义深浅拷贝:
浅拷贝只拷贝表层属性,深层复杂类型共享引用;深拷贝完全拷贝所有层级,切断引用,修改拷贝对象不影响原对象。
- 从 Object.assign () 讲浅拷贝:
说清它的特性(拷贝可枚举属性、返回目标对象)、浅拷贝本质(深层共享引用),再提扩展运算符、slice()等其他浅拷贝方法,以及使用场景(无需修改深层结构时用)。
- 讲深拷贝的两种方式:
-
- 便捷方案:JSON.parse(JSON.stringify()),优点是简单,缺点是处理不了 undefined/function/ 循环引用;
-
- 手写方案:基础版用递归遍历,优化版用WeakMap解决循环引用,重点解释WeakMap的弱引用优势(避免内存泄漏)。
- 结合业务场景收尾:
比如 "简单场景用 JSON 方法,需要处理循环引用或特殊类型时,用手写的深拷贝;不需要修改深层属性时,优先用浅拷贝提升性能"。
按这个思路答,既能覆盖所有核心考点,又能体现你 "从基础到应用" 的思考逻辑 ------ 面试官想要的,就是这种有条理、有深度的回答。