前端面试必问:深浅拷贝从基础到手写,一篇讲透

深浅拷贝是前端面试的 "常驻嘉宾"------ 它不仅考察你对 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 深拷贝的原理与优势

它的逻辑很简单:

  1. JSON.stringify(obj):将对象 "序列化" 为 JSON 字符串(把 JS 数据类型转成字符串格式);
  1. 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(拷贝成功,无栈溢出)

四、面试答题思路总结

当面试官问 "谈谈你对深浅拷贝的理解" 时,别零散地回答,按这个框架说,逻辑清晰又全面:

  1. 先定义深浅拷贝

浅拷贝只拷贝表层属性,深层复杂类型共享引用;深拷贝完全拷贝所有层级,切断引用,修改拷贝对象不影响原对象。

  1. 从 Object.assign () 讲浅拷贝

说清它的特性(拷贝可枚举属性、返回目标对象)、浅拷贝本质(深层共享引用),再提扩展运算符、slice()等其他浅拷贝方法,以及使用场景(无需修改深层结构时用)。

  1. 讲深拷贝的两种方式
    • 便捷方案:JSON.parse(JSON.stringify()),优点是简单,缺点是处理不了 undefined/function/ 循环引用;
    • 手写方案:基础版用递归遍历,优化版用WeakMap解决循环引用,重点解释WeakMap的弱引用优势(避免内存泄漏)。
  1. 结合业务场景收尾

比如 "简单场景用 JSON 方法,需要处理循环引用或特殊类型时,用手写的深拷贝;不需要修改深层属性时,优先用浅拷贝提升性能"。

按这个思路答,既能覆盖所有核心考点,又能体现你 "从基础到应用" 的思考逻辑 ------ 面试官想要的,就是这种有条理、有深度的回答。

相关推荐
CaptainDrake3 小时前
React 中 key 的作用
前端·javascript·react.js
全栈技术负责人3 小时前
移动端富文本markdown中表格滚动与页面滚动的冲突处理:Touch 事件 + 鼠标滚轮精确控制方案
前端·javascript·计算机外设
前端拿破轮3 小时前
从零到一开发一个Chrome插件(二)
前端·面试·github
珍宝商店3 小时前
Vue.js 中深度选择器的区别与应用指南
前端·javascript·vue.js
天蓝色的鱼鱼3 小时前
Next.js 预渲染完全指南:SSG vs SSR,看完秒懂!
前端·next.js
月出3 小时前
无限循环滚动条 - 左出右进
前端
aiwery3 小时前
实现带并发限制的 Promise 调度器
前端·算法
熊猫片沃子3 小时前
浅谈Vue 响应式原理
前端
货拉拉技术3 小时前
微前端中的错误堆栈问题探究
前端·javascript·vue.js