【每日一面】实现一个深拷贝函数

news/2025/11/3 21:23:54/文章来源:https://www.cnblogs.com/keepsmart/p/19188341

基础问答

问:知道浅拷贝和深拷贝吗?为什么要用深拷贝?

答:拷贝,可以认为是赋值,对于 JavaScript 中的基础类型,如 string, number, null, boolean, undefined, symbol 等,在赋值给一个变量的时候,是直接拷贝值给变量,而对于引用类型,如 object, array, function 等,则会拷贝其引用(地址)。
使用深拷贝,是为了避免操作公共对象的时候,影响到其他使用该对象的组件。

扩展延伸

一个拷贝函数,可以直接评估出来你对 JavaScript 基础能力掌握水平。

在理解浅拷贝和深拷贝前,需先明确拷贝的本质。在 JavaScript 中数据类型分为基本类型(string、number、boolean、null、undefined、symbol、bigint)和引用类型(object、array、function 等),这两种类型在内存中存储方式是不一样的:

  • 基本类型:值直接存储在栈内存中,赋值时直接拷贝值。
  • 引用类型:值存储在堆内存中,栈内存仅存储指向堆内存的引用地址,赋值时仅拷贝引用地址(而非实际值)。

所以,根据这两种存储方式很容易想到,浅拷贝和深拷贝的区别就在于 是否递归复制嵌套的引用类型。 这里给出一个简单的定义:

  • 浅拷贝(Shallow Copy):仅复制对象的表层属性,若属性值为引用类型(如嵌套对象、数组),则拷贝的是引用地址(引用地址就是表层属性),新旧对象共享嵌套数据。
  • 深拷贝(Deep Copy):递归复制对象的所有属性,包括嵌套的引用类型,新旧对象完全独立,修改拷贝后的对象不会影响原始对象的数据。

实现方式

浅拷贝

浅拷贝适用于无嵌套引用类型或无需独立嵌套数据的场景,实现方式简单,性能开销小。

  1. 浅拷贝对象 Object.assign()
    Object.assign(target, ...sources) 方法将源对象的可枚举属性复制到目标对象,最后返回的是目标对象,使用这个方法时要注意:该方法仅拷贝对象自身属性(不包含继承属性),嵌套的对象仅拷贝引用,示例如下:
const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);// 测试基本类型属性:修改不影响原对象
shallowCopy.a = 100;
console.log(obj.a); // 输出:1(原对象不变)// 测试嵌套对象:修改会影响原对象
shallowCopy.b.c = 200;
console.log(obj.b.c); // 输出:200(原对象被修改)
  1. 浅拷贝数组 Array.prototype.slice()Array.prototype.concat()
    这两个方法返回的都是新数组(不在原数组上操作),示例如下:
const arr = [1, [2, 3]];
const shallowCopy1 = arr.slice(0); // 方法1:slice
const shallowCopy2 = [].concat(arr); // 方法2:concat// 测试基本类型元素:修改不影响原数组
shallowCopy1[0] = 100;
console.log(arr[0]); // 输出:1(原数组不变)// 测试嵌套数组:修改会影响原数组
shallowCopy2[1][0] = 200;
console.log(arr[1][0]); // 输出:200(原数组被修改)
  1. 扩展运算符 ...
    这个是 es6 新增的运算符,可以用于对象和数组的浅拷贝,语法相较于上面两种方式比较简单,示例如下:
// 对象浅拷贝
const obj = { a: 1, b: { c: 2 } };
const shallowObj = { ...obj };// 数组浅拷贝
const arr = [1, [2, 3]];
const shallowArr = [...arr];

深拷贝

深拷贝适用于包含嵌套引用类型且需要完全独立副本的场景,实现复杂度较高,需处理递归、循环引用等边界情况。属于前端八股面试必须准备的一个问题。

  1. 序列化方式拷贝 JSON.parse(JSON.stringify())
    利用 JSON 序列化与反序列化实现深拷贝,语法简单,多数时候够用。
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const deepCopy = JSON.parse(JSON.stringify(obj));// 测试嵌套对象:修改不影响原对象
deepCopy.b.c = 200;
console.log(obj.b.c); // 输出:2(原对象不变)

但是这个方式有一定的局限性:

  • 不能拷贝函数(JSON不支持)
  • 不能拷贝 undefinedSymbol 类型
  • 不能处理循环引用
  • 不支持 BigInt 类型
  • 对于日期对象和正则对象,有特殊处理,解析后可能得不到我们想要的结果
  1. 自定义实现拷贝函数
    思路:遍历对象,每一次遍历过程中判断是否是引用类型(对象或数组),如果是,则递归的调用拷贝函数,若不是,则直接赋值进行下一步。
function deepCopy(target) {// 基本类型直接返回if (target === null || typeof target !== 'object') {return target;}// 区分数组和对象let copy;if (Array.isArray(target)) {copy = [];} else {copy = {};}// 遍历属性并递归拷贝for (const key in target) {if (target.hasOwnProperty(key)) {// 递归处理引用类型copy[key] = deepCopy(target[key]);}}return copy;
}// 测试
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const copyObj = deepCopy(obj);
copyObj.b.c = 200;
console.log(obj, copyObj, obj === copyObj, obj.b.c); // 对比输出结果,可以发现两个对象是不同的
copyObj.d[0] = 300;
console.log(obj, copyObj, obj === copyObj, obj.d[0]); // 同上

但是这个没有处理边界情况,主要是两种情况:

  • 循环应用
    循环引用指对象引用自身(如 obj.self = obj),直接递归会导致无限循环栈溢出。可以用 WeakMap 存储已拷贝的对象,避免在递归过程中重复拷贝。

  • 特殊对象
    类似于 Date,RegExp 的对象,需要我们手动特殊处理(根据类型直接 new)
    完整的深拷贝示例:

function deepCopy(target, hash = new WeakMap()) {// 基本类型直接返回if (target === null || typeof target !== 'object') {return target;}// 处理循环引用:若已拷贝过,直接返回缓存的副本if (hash.has(target)) {return hash.get(target);}let copy;// 处理Dateif (target instanceof Date) {copy = new Date(target);hash.set(target, copy);return copy;}// 处理RegExpif (target instanceof RegExp) {copy = new RegExp(target.source, target.flags);copy.lastIndex = target.lastIndex; // 保留lastIndex属性hash.set(target, copy);return copy;}// 处理数组和对象if (Array.isArray(target)) {copy = [];} else {// 处理普通对象(包括自定义对象)copy = new target.constructor(); // 保持原型链}// 缓存已拷贝的对象,解决循环引用hash.set(target, copy);// 遍历属性并递归拷贝// 处理Mapif (target instanceof Map) {target.forEach((value, key) => {copy.set(key, deepCopy(value, hash));});return copy;}// 处理Setif (target instanceof Set) {target.forEach(value => {copy.add(deepCopy(value, hash));});return copy;}// 处理普通对象和数组的属性for (const key in target) {if (target.hasOwnProperty(key)) {copy[key] = deepCopy(target[key], hash);}}return copy;
}// 测试循环引用
const obj = { name: 'test' };
obj.self = obj; // 循环引用
const copyObj = deepCopy(obj);
console.log(copyObj.self === copyObj, copyObj === obj, obj, copyObj);// 测试特殊对象
const date = new Date();
const copyDate = deepCopy(date);
console.log(copyDate instanceof Date, copyDate === date, date, copyDate);const reg = /abc/gim;
reg.lastIndex = 10;
const copyReg = deepCopy(reg);
console.log(copyReg, reg);

差异对比

这里我简单总结一个表来让你快速理解二者异同:

对比方向 浅拷贝 深拷贝
拷贝层级 仅拷贝对象表层属性 递归拷贝所有层级(包括嵌套的引用类型)
内存占用 较小(共享嵌套对象的内存) 较大(完全复制所有数据,独立占用内存)
性能开销 低(无需递归,操作简单) 高(递归处理,需处理边界情况)
拷贝前后对象的独立性 表层属性独立,嵌套引用类型共享 完全独立,新旧对象无任何关联
适用场景 无嵌套引用类型、性能优先、无需独立嵌套数据的情况,简单来说,不需要前后独立的,都可以直接用浅拷贝 有嵌套引用类型、需完全隔离数据、修改不能相互影响的情况
实现复杂度 简单(可通过原生方法或简单遍历实现) 复杂(需处理递归、循环引用、特殊对象类型)

面试追问

  1. 直接使用 = 赋值算浅拷贝还是深拷贝?
    都不是,赋值运算符只是将一个值或者引用赋给一个变量,对于基本类型,赋值运算符是直接复制这个值给变量,对于引用类型,赋值运算符则是复制引用给变量,而非对象本身。
    这个和浅拷贝的定义略有差异。

  2. 实现一个浅拷贝函数?
    思路就是,直接遍历浅层对象(第一层),赋给新的对象。

function shallowCopy(target) {// 区分目标是数组还是对象if (Array.isArray(target)) {const copy = [];for (let i = 0; i < target.length; i++) {copy[i] = target[i];}return copy;} else if (target !== null && typeof target === 'object') {const copy = {};// 仅拷贝自身可枚举属性for (const key in target) {if (target.hasOwnProperty(key)) {copy[key] = target[key];}}return copy;} else {// 基本类型直接返回(无需拷贝)return target;}
}// 测试
const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
const copyObj = shallowCopy(obj);
copyObj.b.c = 200;
console.log(obj.b.c); // 输出:200(嵌套对象共享引用)
  1. 深拷贝的时候,怎么特殊处理函数类型?
    函数属于引用类型,通常不需要深拷贝,因为函数体是改不了的,通常直接复制引用就行了。
    如果面试时强烈要求你深拷贝,可以直接使用 toString() + eval 实现,但可能随之而来的会将话题转到 eval 上来问词法作用域、严格模式、安全问题等等,一般是来转换个话题。

  2. 实际开发的时候,有经常用这两种模式吗?举个场景说明一下

  • 前端分页,displayData 通常是直接通过 slice 获取原始列表的一部分数据,由于不需要操作,所以也不需要深拷贝
  • 接口传参,有时候我们为了方便,会在请求数据信息之后,直接将这个返回的对象赋值给某个地方,之后再提交的时候,由于接口要求的信息不同,我们有可能会直接操作这个返回对象,导致使用返回对象的地方出现变化,这种情况就需要深拷贝。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/954915.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【AI说Rust 01】Rust 的学习路线

Rust 以其卓越的性能和内存安全性吸引了众多开发者。虽然它的学习曲线相对陡峭,但一份清晰的学习路线能让你事半功倍。下面这个路线图汇总了主流的学习阶段和资源,希望能帮你从零开始,逐步进阶。 flowchart LRA[Rus…

若依后端验证码实现

先去看前端的 参考详细讲解视频:https://www.bilibili.com/video/BV1HT4y1d7oA?spm_id_from=333.788.player.switch&vd_source=886219f6fb49f459fbfc8b80a8b39f3f&p=3 登录 前端请求为http://localhost/dev-…

解码LVGL事件

LVGL 事件系统 事件是 LVGL 响应用户操作(如点击、滑动)或控件状态变化的核心机制,通过 “事件绑定 - 回调函数” 实现交互逻辑。 事件核心特点多绑定支持:一个回调函数可绑定多个对象(如一个 “计数回调” 绑定两…

11.3号学习内容

阅读模型压缩的论文| https://doi.org/10.48550/arXiv.2010.03954 | header | | ----------------------------------------- | ------ | | | |

P11771 题解

blog。虽然糖丸了,但是卡了还是半天卡过去了。感谢出题人开 2s /kt!!最显然的暴力是,考虑直接算每个 \(i,j,k\) 的贡献。\(p_{i}\le p_k\wedge p_j\le p_k\):贡献为 \(0\)。 \(p_{i}>p_k\wedge p_j\le p_k\):…

MySQL排序算法

一、概述 ORDER BY的核心功能,是按照指定的单个或多个字段,对SELECT查询返回的结果集进行升序(ASC,默认)或降序(DESC)排列,以满足业务对数据有序性的需求。但要判断ORDER BY的实际执行效率,最直接的工具是EXP…

CSP-S 2025 饭堂寄

省流:\(100+48+0+0=148\),爆炸。 Day -2 考试前几天竟然发现有些感冒了。 Day -1 考试前一天晚上睡得比较晚,因为回到家都接近 11 点钟了。 Day 1 早上起来已经 9:30 了,起来开始打板子,其实这个时候已经感觉状态…

如何在github上使用github免费域名下预览自己的项目

一、新建自己的工程然后上传自己的工程文件,有首页的话记下首页的路径。 二、点击Settings 点击Pages,填写自己要访问的index.html文件路径

在ROS中安装PX4依赖实现Gazebo仿真

在ROS中安装PX4依赖实现Gazebo仿真最近这几天在做一个无人机项目,在配置gazebo仿真时出现了找不到px4的问题,但是又无法直接安装,需要自行编译 简单做一下记录 sudo apt install ninja-build exiftool ninja-build …

20232314 2024-2025-1 《网络与系统攻防技术》实验四实验报告

一、实验内容 1、恶意代码文件类型标识、脱壳与字符串提取 对提供的rada恶意代码样本,进行文件类型识别,脱壳与字符串提取,以获得rada恶意代码的编写作者,具体操作如下: (1)使用文件格式和类型识别工具,给出ra…

二、驱动基础(基于北京迅为电子)

一、基础Linux驱动的分类:字符设备(顺序访问)、块设备(随机访问)、网络设备(数据包收发) Linux内核源码的目录架构:arch(架构相关)、block(块设备)、crypto(加密算法)、Documentation(官方文档)、driv…

Linux驱动开发学习日记(一)

Linux驱动开发学习日记(一)整完无人机项目之后进行更新,具体怎么写还没想好,现在学的也比较迷糊

Windows 路由表详解

Windows 路由表详解windows 路由表详解 查看ip信息 字段说明IPv4 Address: ipv4地址,用于标识网络中的主机Subnet Mask: 子网掩码,分为 连续的1 和 连续的0 两部分, 可以简写为 /n, 例如 /24,表示高24位为1,剩下为…

微软 Foundry Local - 本地 AI 推理解决方案

微软在其 2025 Build 大会上发布了 Foundry Local,能够在本地设备上执行 AI 推理,意味着可以利用本地的 AI 算力,如:CPU/GPU/NPU;也让用户在隐私方面得到了充足的保障,还能有改善成本效益!Foundry Local 默认除…

如何启用cycloneDDS的iceoryx

共享内存交换 — Eclipse Cyclone DDS,0.11.0 首先我们需要先下载安装iceoryx,因为cycloneDDS如果要使用共享内存传输是依赖于这个插件的。顺带一提,只有同一节点的不同进程间会使用到共享内存,cycloneDDS是根据如…

老化车

老化车非常好 👍,这个问题在电子制造和测试领域里很关键。 “老化车”(又叫 Burn-in Cart 或 Aging Rack)是用于电子产品在出厂前做 老化测试(Burn-in Test) 的一种设备或平台。 下面我给你系统讲清楚 👇🧭…

Android Studio 2025.2.1 汉化中文包临时解决方案

打开 JetBrains 官网 Chinese ​(Simplified)​ Language Pack / 中文语言包 下载最新版 242.152 版本插件将 zh.242.152.jar 文件解压出来用压缩包工具打开 zh.242.152.jar 找到目录 META-INF 并打开用文本编辑工具打…

Markdown 学习训练

Markdown 学习训练 引用(使用>)这是我的第一篇博客,本篇博客是参照狂神说课程进行学习,目的是为了练习markdown使用语法。主要包含各级标题、字体、图片、代码块、超链接、表格使用语法练习。具体可前往typora官网…

jmeter设置中文页面的两种方法

JMeter设置中文界面有两种方法:临时设置(仅当前会话有效)和永久设置(修改配 置文件后永久生效)。 设置方法 1.临时设置(关闭后恢复英文): 打开JMeter,点击菜单栏“Options”→“Choose Language”→选择“Chi…

win10 下运行aoe2,报错,应用程序无法正常启动 0xc000022

控制面板 →搜索“启用或关闭 Windows 功能”找到并勾选 “旧版组件” → “DirectPlay” → 确定 → 重启电脑。