当前位置: 首页 > news >正文

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

img

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

🏠个人主页:尘觉主页

文章目录

  • 浅复制和深复制?怎样实现深复制?
    • 一、概念(直观理解)
    • 二、常见快捷方法与它们的优缺点
    • 三、实现深拷贝的正确思路(要点)
    • 四、推荐的 `deepClone` 实现(注释详尽)
    • 五、示例验证
      • 面试写法简介
        • 存在的问题
    • 六、常见问题与注意事项(FAQ)
    • 七、总结

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


一、概念(直观理解)

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

    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 实现(注释详尽)

/*** 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;// 处理 Dateif (isDate(src)) {dst = new Date(src.getTime());map.set(src, dst);return dst;}// 处理 RegExpif (isRegExp(src)) {const flags = src.flags; // g, i, m, u, s, ydst = new RegExp(src.source, flags);map.set(src, dst);return dst;}// 处理 Mapif (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;}// 处理 Setif (isSet(src)) {dst = new Set();map.set(src, dst);for (const v of src.values()) {dst.add(deepClone(v, map));}return dst;}// 处理 Arrayif (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); // 包含不可枚举和可枚举、symbolfor (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;
}

五、示例验证

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  (循环引用保持)

面试写法简介

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连支持一下,创造不易您们的支持是我的动力🤞

img

http://www.dtcms.com/a/533185.html

相关文章:

  • 浙江省城乡建设厅网站网页制作软件排行榜
  • h5游戏网站开发wordpress 固定链接结构出错
  • 网站网页设计制作教程建外贸网站的
  • 手机创建自己网站网站底部 图标
  • AI驱动下的(期现交易员的)基本面研究
  • 地方网站商城怎么做灌南县规划局网站理想嘉苑规划建设
  • 淘宝客网站如何做排名设计可以在哪个网站接单
  • 【小宁的学习日记2 C语言】C语言判断
  • cp网站开发多少钱wordpress获取当前目录父目录id
  • 一个空间可以放几个网站深圳市招聘网站
  • 上海网站建设公司官网如何做网站推广最有效
  • 安卓开发如何实现自定义View
  • 【netty】基于主从Reactor多线程模型|如何解决粘包拆包问题|零拷贝
  • python数据清洗与预处理指南
  • 【模型评测】主流编程大模型QML编程横向对比
  • 网站怎么做团购什么是网络营销网络营销与电商营销有什么区别
  • Go语言:常量设置的注意事项
  • 网络营销导向企业网站建设的一般原则包括徐州网站排名系统
  • 基本魔法语言分支和循环 (二) (C语言)
  • 根目录下两个网站怎么做域名解析科技进步是国防强大的重要的保证
  • 微网站建设c品牌网站设计流程
  • 有哪些cua模型 Computer-Using Agent
  • 网站建设方案模板高校物流信息网站
  • 网工综合知识总结
  • 科技前沿七日谈:从AI普惠到硬件创新,技术正重塑产业边界
  • 初识AES
  • (五)图文结合-详解BLE连接原理及过程
  • 资产管理公司网站建设费用怎么入账电子商务行业发展趋势及前景
  • 机器学习日报05
  • 成都公园城市建设局网站seo诊断分析工具