面试-浅复制和深复制?怎样实现深复制详细解答

😀前言

在前端开发中,我们经常需要"复制"一个对象或数组,比如在修改数据时不想影响原始数据、在 Redux 状态管理中保持数据不可变性、或者在组件中需要生成一个独立的副本。这时就涉及到两个常听到的概念------浅拷贝(Shallow Copy) 和 深拷贝(Deep Copy)。

🏠个人主页:尘觉主页

文章目录

浅复制和深复制?怎样实现深复制?


一、概念(直观理解)

  • 浅拷贝(shallow copy) :只复制对象第一层的属性。如果属性值是一个引用类型(比如对象、数组),浅拷贝只复制引用(指向同一块内存)。

    例子:

    js 复制代码
    const a = { x: 1, y: { z: 2 } };
    const b = { ...a }; // 或 Object.assign({}, a)
    b.y.z = 99;
    console.log(a.y.z); // 99 ------ 因为 a.y 和 b.y 指向同一个对象
  • 深拷贝(deep copy / deep clone):把对象的每一层都复制一份,引用类型会被递归复制,最终得到一个完全独立于原对象的新值。修改新对象不会影响原对象。


二、常见快捷方法与它们的优缺点

  1. JSON.parse(JSON.stringify(obj))

    • 优点:写法极简,性能在一些场景下不错。
    • 缺点:无法复制函数、Date(会变成字符串)、RegExpMapSetundefinedSymbol、以及会丢失 prototype、不可处理循环引用(会报错)。
    • 结论:适用于简单纯 JSON 数据(仅包含对象、数组、数字、字符串、布尔、null)。
  2. 手写递归拷贝(最常见思路)

    • 要解决的问题:类型判断、循环引用、特殊内置类型(Date/RegExp/Map/Set/TypedArray/...)、symbol-key、属性可枚举性/描述符、原型链等。
    • 难点:边界情况多,代码需要谨慎。

三、实现深拷贝的正确思路(要点)

  1. 对象/数组的递归拷贝(区分 Array 与普通 Object)。
  2. 处理 nulltypeof null === 'object',需特殊判断)。
  3. 处理常见内置类型:Date, RegExp, Map, Set(另外还可以支持 TypedArray)。
  4. 使用 WeakMap 跟踪已经拷贝过的源对象 => 解决循环引用并避免重复拷贝。
  5. 复制 Symbol 键不可枚举属性/属性描述符 取决于要求(下面实现会拷贝可枚举键与 Symbol 键;如果要完整复制描述符也可扩展)。
  6. 函数通常保持原引用(不深拷贝函数体),因为"复制函数"通常没有意义(除非做特殊处理)。

四、推荐的 deepClone 实现(注释详尽)

js 复制代码
/**
 * deepClone - 支持:Object, Array, Date, RegExp, Map, Set, Symbol-key, 循环引用
 * 不复制:函数的执行上下文(函数保持引用),不会复制原型链上的非自有属性(但保留 __proto__ 指向同一原型)。
 *
 * 性能/复杂度:时间复杂度与对象总节点数 roughly 成正比(O(n)),但处理 Map/Set/Array 等也会遍历其元素。
 */
function isObject(val) {
  return Object.prototype.toString.call(val) === '[object Object]';
}
function isArray(val) {
  return Array.isArray(val);
}
function isDate(val) {
  return Object.prototype.toString.call(val) === '[object Date]';
}
function isRegExp(val) {
  return Object.prototype.toString.call(val) === '[object RegExp]';
}
function isMap(val) {
  return Object.prototype.toString.call(val) === '[object Map]';
}
function isSet(val) {
  return Object.prototype.toString.call(val) === '[object Set]';
}

function deepClone(src, map = new WeakMap()) {
  // 原始类型或函数直接返回(函数按引用处理)
  if (src === null || typeof src !== 'object') return src;

  // 已经拷贝过(处理循环引用)
  if (map.has(src)) return map.get(src);

  let dst;

  // 处理 Date
  if (isDate(src)) {
    dst = new Date(src.getTime());
    map.set(src, dst);
    return dst;
  }

  // 处理 RegExp
  if (isRegExp(src)) {
    const flags = src.flags; // g, i, m, u, s, y
    dst = new RegExp(src.source, flags);
    map.set(src, dst);
    return dst;
  }

  // 处理 Map
  if (isMap(src)) {
    dst = new Map();
    map.set(src, dst);
    for (const [k, v] of src.entries()) {
      // 键也可能是对象或复杂类型,所以递归克隆键和值
      const clonedKey = deepClone(k, map);
      const clonedVal = deepClone(v, map);
      dst.set(clonedKey, clonedVal);
    }
    return dst;
  }

  // 处理 Set
  if (isSet(src)) {
    dst = new Set();
    map.set(src, dst);
    for (const v of src.values()) {
      dst.add(deepClone(v, map));
    }
    return dst;
  }

  // 处理 Array
  if (isArray(src)) {
    dst = [];
    map.set(src, dst);
    for (let i = 0; i < src.length; i++) {
      dst[i] = deepClone(src[i], map);
    }
    return dst;
  }

  // 处理 TypedArrays(可选扩展 -- 这里给出基本处理)
  // 例如 Int8Array, Uint8Array, Float32Array 等
  if (ArrayBuffer.isView(src)) {
    // 对于 TypedArray 或 DataView,复制底层缓冲区
    const ctor = src.constructor;
    dst = new ctor(src);
    map.set(src, dst);
    return dst;
  }

  // 处理普通对象(包括有 Symbol 键的属性)
  // 创建新的对象并保留原型(如果你不想保留原型,可改成 {})
  const proto = Object.getPrototypeOf(src);
  dst = Object.create(proto);
  map.set(src, dst);

  // 获取所有自有属性键:包括字符串键与 Symbol 键
  const keys = Reflect.ownKeys(src); // 包含不可枚举和可枚举、symbol
  for (const key of keys) {
    // 复制属性描述符(保留 writable/configurable/enumerable/get/set)
    const desc = Object.getOwnPropertyDescriptor(src, key);
    if (desc) {
      if (desc.get || desc.set) {
        // 如果是 getter/setter,直接定义描述符(保持行为)
        Object.defineProperty(dst, key, desc);
      } else {
        // 普通值:递归拷贝
        desc.value = deepClone(desc.value, map);
        Object.defineProperty(dst, key, desc);
      }
    }
  }

  return dst;
}

五、示例验证

js 复制代码
const obj111 = {
  a: 1,
  b: {
    c: 2,
    d: {
      e: 3
    },
    f: [1, { a: 1, b: 2 }, 3]
  },
  t: new Date('2020-01-01'),
  r: /abc/gi,
  m: new Map([['k1', { x: 1 }]]),
  s: new Set([1, 2, { y: 9 }]),
  [Symbol('sym')]: 'symValue'
};

// 循环引用测试
obj111.self = obj111;

const cloned = deepClone(obj111);

// 验证
console.log(cloned !== obj111); // true
console.log(cloned.b !== obj111.b); // true
console.log(cloned.b.f[1] !== obj111.b.f[1]); // true
console.log(cloned.t instanceof Date && cloned.t.getTime() === obj111.t.getTime()); // true
console.log(cloned.r instanceof RegExp && cloned.r.source === obj111.r.source); // true
console.log(cloned.m instanceof Map && cloned.m.get('k1') !== obj111.m.get('k1')); // true
console.log(cloned.self === cloned); // true  (循环引用保持)

面试写法简介

js 复制代码
const isObject = (item)=>{
  return Object.prototype.toString.call(item) === '[object Object]';
}
const isArray = (item)=>{
  return Object.prototype.toString.call(item) === '[object Array]';
}

const deepClone=(obj)=>{
    const cloneObj=isArray(obj)?[]:isObject(obj)?{}:'';
    for(let key in obj){
        if(isObject(obj[key])||isArray(obj[key])){
          Object.assign(cloneObj,{
           [key]: deepClone(Reflect.get(obj,key))
          });
        }
        else{
          cloneObj[key] = obj[key];
        }  
    }
    return cloneObj;
}
存在的问题
问题点 原因说明 举例
① 无法处理 null 因为 typeof null === 'object',你的 isObject() 会误判 deepClone({a: null}) 会出错
② 不能处理其他类型 只考虑了数组和对象,像 DateRegExpMapSet 等都拷贝不了 deepClone({t: new Date()}) 会变成空对象
③ 没有处理循环引用 如果对象自己引用自己,会死循环 const a={}; a.self=a; deepClone(a) 报错
④ 使用 for...in 会遍历原型链上的属性 一般我们只想复制对象自身的属性
⑤ 用 Object.assign 每次都创建新对象(性能低) 其实可以直接 cloneObj[key] = deepClone(obj[key])
⑥ 如果不是对象或数组,返回 '' 这会造成函数在意外输入时输出错误类型,不如直接返回原值

六、常见问题与注意事项(FAQ)

  1. 为什么不用 for...in
    for...in 会遍历原型链上的可枚举属性,通常我们只关心对象自身(自有属性)。上面的实现用 Reflect.ownKeys + getOwnPropertyDescriptor 来复制自有属性(包括 Symbol 和不可枚举),并保留属性描述符(可扩展为只复制可枚举的属性,视需求而定)。

  2. 函数如何处理?

    函数会当作普通值返回(保持引用)。通常复制函数体并不有意义。如果你确实需要克隆函数(比如绑定上下文或序列化),那是更复杂/不常见的场景。

  3. 原型链和 constructor?

    上面代码通过 Object.create(proto) 保留了原型。若你想完全保留 constructor、原型上的不可枚举行为或某些特殊行为,需要更复杂的处理。

  4. 性能

    深拷贝比分配引用代价高,若对象非常大或频繁调用,可能影响性能。仅在需要"独立副本"时使用深拷贝。

  5. 还有哪些类型没处理?

    • 几类特殊内置类型(例如 PromiseWeakMapWeakSet)通常无需克隆或无法克隆(WeakMap/WeakSet 的键是弱引用)。
    • DOM 节点、函数闭包环境等无法简单克隆。
    • 如果需要克隆属性访问器(get/set 已保留描述符),但若 get 会访问私有闭包状态,克隆无法复制闭包内部状态。
  6. 如何选择实现?

    • 数据简单且只包含 JSON-friendly 类型:JSON.parse(JSON.stringify(obj))
    • 需要处理循环引用或内置对象:使用上面这种带 WeakMap 的手写实现(或使用成熟库,如 lodash.cloneDeep,会处理很多边界情况并经过优化)。

七、总结

  • 先区分"浅拷贝"和"深拷贝",理解引用类型与原始类型的差别。
  • 对简单数据(纯 JSON)可用 JSON.parse(JSON.stringify(...))
  • 需要健壮、通用方案时,使用递归 + WeakMap 来处理循环引用,并对 Date/RegExp/Map/Set/TypedArray 等做特殊处理。
  • 若对性能或边界行为(原型、不可枚举属性、属性描述符)有更细粒度需求,考虑使用并阅读成熟库(例如 lodashcloneDeep)或根据具体需求扩展实现。

😁热门专栏推荐
想学习vue的可以看看这个

java基础合集

数据库合集

redis合集

nginx合集

linux合集

手写机制

微服务组件

spring_尘觉

springMVC

mybits

等等等还有许多优秀的合集在主页等着大家的光顾感谢大家的支持

🤔欢迎大家加入我的社区 尘觉社区

文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😁

希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🍻

如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🤞

相关推荐
绝无仅有5 小时前
某教育大厂面试题解析:MySQL索引、Redis缓存、Dubbo负载均衡等
vue.js·后端·面试
mapbar_front5 小时前
面试谈薪资指南:掌握主动权的关键策略
面试
chxii5 小时前
ISO 8601日期时间标准及其在JavaScript、SQLite与MySQL中的应用解析
开发语言·javascript·数据库
没逛够5 小时前
Vue 自适应高度表格
javascript·vue.js·elementui
007php0077 小时前
京东面试题解析:同步方法、线程池、Spring、Dubbo、消息队列、Redis等
开发语言·后端·百度·面试·职场和发展·架构·1024程序员节
咖啡の猫8 小时前
Vue初始化脚手架
前端·javascript·vue.js
微笑尅乐8 小时前
洗牌算法讲解——力扣384.打乱数组
算法·leetcode·职场和发展
绝无仅有8 小时前
腾讯面试文章解析:MySQL慢查询,存储引擎,事务,结构算法等总结与实战
后端·面试·github
松间沙路hba8 小时前
面试过程中的扣分项,你踩过几个?
面试·职场和发展