我与深拷贝

news2024/11/15 23:19:28

前言

最近在掘金读到了一篇文章《Radash 能取代 Lodash???真幽默 - 掘金》,文章的评论区讨论起了深拷贝。"深拷贝" 我的"老朋友",还记得在学习我人生中的第二道面试题的时候认识了它,还记得那是一道手写深拷贝的面试题,还记的那个讲题的老师很贱很嚣张,弹幕中戏称他讲的是"嘴硬代码"。不知道为什么想起这些心中莫名的有些忧伤,我想或许是时候总结一下"我与深拷贝的故事",来一场"朝花夕拾"。

一、什么是深拷贝?

1.基本数据类型与引用数据类型的区别

想要理解深拷贝,必须要先知道基本数据类型和引用数据类型的区别。

基本数据类型

基本数据类型的值存储在栈内存中,所以在进行基本数据类型的赋值操作时,前后两个值是相互独立的,请看下面的例子:

let a = 10
let b = a
console.log(a,b)//10 10

这里就进行了一次基本数据类型的赋值操作,将变量a中的基础数据类型值10复制了一份放到了变量b中。此时虽然ab中的值相同(都是10),但它们是相互独立的,因此修改其中一个变量的值并不会影响到另一个变量。

let a = 10
let b = a
b = 20
console.log(a,b)//10 20

引用数据类型

引用数据类型则是保存在堆内存中。与其他语言的不同是,在JS中不可以直接访问堆内存空间中的位置和操作堆内存空间。只能操作对象在栈内存中的引用地址。

引用数据类型在栈内存中保存的实际上是对象在堆内存中的引用地址。因此,在进行引用数据类型的赋值操作时,复制的就不是值了,而是一个引用地址,请看下面的例子:

let a = { age = 10 }
let b = a
console.log(a)//{ age = 10 }
console.log(b)//{ age = 10 }

同样还是将a赋值给b,但这次a中保存的是引用数据类型的值(一个对象),因此在赋值之后,修改ab对象中的属性,另一个也会跟着改变,因为实际上它俩都是同一个数据。

let a = { age = 10 }
let b = a
b.age = 20
console.log(a)//{ age = 20 } 
console.log(b)//{ age = 20 }

2.赋值与拷贝的区别

这里探讨一个问题:什么是"拷贝"?

我之前一直认为上面讲到的赋值就是拷贝 ( 例如: b = a ) ,在查阅了一些相关的资料后我发现似乎并不是这样。

赋值

按照我的理解,赋值是指在栈内存中将一个变量值复制一份存储到另一个变量中,如果是普通数据类型复制的就是数据,如果是引用数据类型复制的就是引用地址

拷贝

拷贝则是指在堆内存中开辟一个新的区域,将原有的一个引用数据类型值复制一份存储到这个新区域中,此时栈内存中的两个变量存储的是两个不同的引用地址

总结

所以这里我就得出两个重要的结论:

  1. 赋值与拷贝不是一回事,对于引用数据类型来说,赋值只复制栈中的引用地址,不改变堆中存储的对象;而拷贝则不复制引用地址,转而复制堆中存储的对象。
  2. 只有引用数据类型才存在拷贝的概念(包括下面要讲的深拷贝和浅拷贝),基本数据类型不存在拷贝。

3.浅拷贝与深拷贝的区别

拷贝可以分为两种,一种是不完全拷贝即浅拷贝,另一种是完全拷贝即深拷贝

浅拷贝

浅拷贝是指在堆中复制对象时只复制第一层属性,这种复制是不完全的、是浅层的,对象(数组)内部的嵌套的对象(数组)只会保留引用地址,也就是说对象中普通数据类型的属性会被复制,而引用数据类型的属性则只会被赋值(只会复制引用地址)。因此这些嵌套对象是共享一块内存的,会相互影响。

浅拷贝的实现方式

浅拷贝的实现方式包括:对象/数组展开运算, Object.assign, Object.create, Array.prototype.concat, Array.prototype.slice等。

  let a = { age: 10 }
  let b
  //i-展开运算符(对象和数组)
  a = { age: 10 , info:{ value:1 } }
  b = { ...a }
  b.age = 20
  b.info.value = 2
  console.log(a, b)//{ age: 10 , info: { value: 2 } }  { age: 20 , info: { value: 2 } }
  // i- Object.assign 对象混入
  a = { age: 10 , info:{ value:1 } }
  b = Object.assign({}, a)
  b.age = 20
  b.info.value = 2
  console.log(a, b)//{ age: 10 , info: { value: 2 } }  { age: 20 , info: { value: 2 } }
  //i- Object.create  寄生式继承
  a = { age: 10 , info:{ value:1 } }
  b = Object.create(a)
  b.age = 20
  b.info.value = 2
  console.log(a, b)//{ age: 10 , info: { value: 2 } }  { age: 20 , info: { value: 2 } }

  //❤️🐼🐸🐽🐻🐻‍❄️🐔🐨

  /**
   * i-   arr.concat() 数组合并
   * */
  a = [1, { age: 10 }]
  b = [].concat(a)
  b[0] = 10
  b[1].age = 20
  console.log(a, b) //[ 1 , { age: 20 }]  [ 10 , { age: 20 }]
  /**
   * i- arr.slice() 数组剪切
   */
  a = [1, { age: 10 }]
  b = a.slice()
  b[0] = 10
  b[1].age = 20
  console.log(a, b) //[ 1 , { age: 20 }]  [ 10 , { age: 20 }]

深拷贝

深拷贝则是会对对象中的每一层都进行拷贝(包括嵌套的对象),因此拷贝前后的两个对象完全独立互不影响。

对比赋值、浅拷贝和深拷贝

两个变量是否指向同一对象

修改基本类型的属性是否会影响另一个变量

修改引用类型的属性是否会影响另一个变量

赋值

浅拷贝

深拷贝

二、如何实现深拷贝?

这部分才是这篇文章我真正想讨论的内容,我会首先讲述我自己用过的深拷贝方法,最后再补充其他的方法。

1.手写深拷贝

在前言中提到我最早接触深拷贝就是在一个讲手写深拷贝的面试题课中,所以手写深拷贝就是我最早接触的深拷贝方法。

实现深拷贝的基本原理就是:基本数据类型不存在赋值问题,而对象数组等引用数据类型又是由基本数据类型所组成的。

因此基本思路就是:遍历对象/数组,如果属性为基本数据类型就直接复制,如果属性为另一个对象/数组,那就进行递归处理。

当然关于深拷贝还有许多的细节需要去处理可千万不要小看它,这里我推荐文章《如何写出一个惊艳面试官的深拷贝?》它里面详细的讲述了许多的细节问题。

手写深拷贝有着无限的可能,我们可以不断的打磨使其趋近完美,在这个过程还能够锻炼我们的能力,不过...

2.JSON.parse(JSON.stringify())

手写虽好但毕竟实际工作不等同于面试,很快我就发现了手写深拷贝方法就很麻烦,毕竟每个项目都重写一遍,或者重新复制一份真的很麻烦。所以很快我就抛弃了手写深拷贝,转用JSON.parse(JSON.stringify())

用法介绍

这种方法利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

  let a = { age: 10, info: { value: 1 } }
  let b

  // JSON方法实现深拷贝
  b = JSON.parse(JSON.stringify(a))
  b.info.value = 2
  console.log(a, b) //{ age: 10 , info: { value: 1 } }  { age: 20 , info: { value: 2 } }

局限性

当然这个方法有一定的局限性。

比如说如果克隆的对象存在循环引用的话,就会报错

  let a = { age: 10, info: { value: 1 } }
  let b

  a.target = a
  b = JSON.parse(JSON.stringify(a))

又比如无法正确的克隆函数和正则

  let a = { age: 10, info: { value: 1 } }
  let b

  a.go = function () {
    console.log('我走了')
  }
  a.check = /^[0-9]/
  b = JSON.parse(JSON.stringify(a))
  console.log(a, b)

可以看到克隆之后,函数直接消失了,正则变为了一个空对象{}

3.lodash的_.cloneDeep方法

在工作之后发现用的的最多的还是各种工具库中的深拷贝方法。我常用的就是lodash的cloneDeep

import { cloneDeep } from 'lodash-es'

let a = { age: 10, info: { value: 1, check: /^[0-9]/ }, go: function () {} }
a.target = a
let b = cloneDeep(a)
console.log(b);

4.structuredClone

最近我了解到在前几年JS终于也有了自己内置的深拷贝方法structuredClone

使用

我们来尝试一下:

let a = { age: 10, info: { value: 1, check: /^[0-9]/ }, go: function () {} }
a.target = a
let b = structuredClone(a)
console.log(b)

结果报错了,显示structuredClone无法克隆函数。那去掉函数再试一下:

let a = { age: 10, info: { value: 1, check: /^[0-9]/ }}
a.target = a
let b = structuredClone(a)
console.log(b)

这次顺利的拷贝出来了,可以看到structuredClone可以克隆正则,也可以处理循环引用。

局限性

structuredClone并非完美的它还有一些局限性:

  • 原型:如果你使用structuredClone克隆类实例,你将获得一个普通对象作为返回值,因为结构化克隆会丢弃对象的原型链。
  • 不可克隆:有些值不是结构化可克隆的,尤其是Error、 DOM 节点 和 Function。尝试这样做将引发 DataCloneError 异常。
  • 属性描述符:setter和getter(以及类似元数据的功能)不会被复制。例如,如果使用属性描述符将对象标记为只读,则复制后的对象中是可读写(默认配置)。
  • RegExp:RegExp对象的lastIndex字段不会保留。

参考资料

  1. JS的基本数据类型和引用数据类型 - 掘金
  2. JavaScript提升:掌握深拷贝与浅拷贝的技巧及如何手写深拷贝 - 掘金
  3. 浅拷贝与深拷贝 - 掘金
  4. 如何写出一个惊艳面试官的深拷贝?
  5. 最新HTML规范——structuredClone深拷贝函数,能取代JSON或者lodash吗? - 掘金

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

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

相关文章

CSS 画一个三角形

一、前言 在前端开发的时候,我们有时候会需要用到一个三角形的形状,比如地址选择或者播放器里面播放按钮 通常情况下,我们会使用图片或者svg去完成三角形效果图,但如果单纯使用css如何完成一个三角形呢? 实现过程似…

物理学视角讲解diffusion生成模型——隐扩散模型

https://zhuanlan.zhihu.com/p/692996885 https://zhuanlan.zhihu.com/p/693255617 前面两篇文章介绍了扩散过程,同时实现了1维、2维混合高斯扩散、逆扩散,通过模型预测得分函数来实现逆扩散推理。这个章节介绍工业界使用的文本生成图扩撒模型&#xff1…

基于SSM+Jsp+Mysql的多人命题系统

开发语言:Java框架:ssm技术:JSPJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包…

frp改造Windows笔记本实现家庭版免费内网穿透

文章目录 前言frp原理Windows服务端IP检验IP固定软件下载端口放行端口映射开机启动 NAS客户端端口查询软件下载端口检验穿透测试自启设置 Ubuntu客户端软件下载后台启动 后记 前言 之前一直用花生壳远程控制一个服务器,但最近内网的网络策略似乎发生了变化&#xf…

InfiniGate自研网关实现思路二

5.HTTP请求参数解析 解析 HTTP 网络请求的参数信息,包括;GET/POST,以及应对不同 Content-Type 类型的处理。 HTTP 接口请求的参数需要解析成可以匹配到 RPC 接口的入参信息,所以通常为了方便控制一般只支持 RPC 接口单个对象入参…

「sentinel」流量控制组件的应用

「sentinel」流量控制组件的应用 Sentinel版本QPS 一、初识Sentinel1、Sentinel2、Sentinel 和 Hystrix对比3、雪崩问题 二、环境搭建1、下载安装Sentinel2、微服务整合Sentinel 三、流量控制1、簇点链路2、流控设置3、流控模式直接关联链路 4、流控效果流控效果解释 四、热点限…

C#通用类库封装实战

数据库查询 特性方式获取数据库列的别名 数据库更新 使用简单工厂配置的方式

C++ stl容器stack,queue,priority_queue的底层模拟实现

目录 前言: 文档借鉴:Reference - C Reference 1.deque a.deque的结构特点: b.deque的迭代器结构: c.面试题: 2.stack 3.queue 4.仿函数 5.priority_queue 总结: 前言: 本篇一共简单…

【QT学习】8.qt事件处理机制,事件过滤器,自定义事件

1.qt事件处理机制 事件处理: 当用户移动鼠标的时候 ,创建一个 鼠标移动事件对象 然后把这个对象放到 事件队列里面去,事件管理器 从队列中 取出事件,然后 调用其对应的事件处理函数。 多态机制: &#x…

靠谱的婚恋平台有哪些?青藤之恋、二狗、百合网、珍爱网等深度测评

哇塞,恋爱和结婚对于年轻人来讲可是超级重要的大事呢!不过呀,找到一个稳稳当当的婚恋平台可不简单哟!那么,到底哪个婚恋平台最靠得住呢? 丛丛: 这可是我用了好久好久的脱单交友小程序嘞&#xf…

MySQL中explain的用法

执行结果各字段的含义 EXPLAIN SQL语句 如: EXPLAIN SELECT * FROM test 执行结果: 列名描述id在一个大的查询语句中每个SELECT关键字都对应一个 唯一的idselect_typeSELECT关键字对应的那个查询的类型table表名partitions匹配的分区信息type针对单表…

机器学习预测汽车油耗效率 MPG

流程 数据获取导入需要的包引入文件,查看内容划分训练集和测试集调用模型查看准确率 数据获取 链接:https://pan.baidu.com/s/1KeIJykbcVpsfEk0xjhiICA?pwd30oe 提取码:30oe --来自百度网盘超级会员V1的分享导入需要的包 import pandas as pd imp…

【Spring Boot】掌握Spring Boot:深入解析配置文件的使用与管理

💓 博客主页:从零开始的-CodeNinja之路 ⏩ 收录文章:【Spring Boot】掌握Spring Boot:深入解析配置文件的使用与管理 🎉欢迎大家点赞👍评论📝收藏⭐文章 目录 Spring Boot 配置文件一. 配置文…

重新总结一下以前写过的“波特率”!单片机常见的通信速率分析!

文章目录 如题以前文章新的总结如题 波特率是单片机中描述通信速率的一个单位,比如串口通信、SPI通信、IIC通信、LIN通信、CAN通信等等,现在重新总结一下涉及到波特率的一些知识点。 以前文章 上面是存储的单位换算方式 这是通信速率的换算方式 新的总结 波特率的英文是…

画家-qt-surce

void GraphicView::paintEvent(QPaintEvent *pe) { QPainter painter(viewport()); painter.setRenderHint(QPainter::SmoothPixmapTransform);//升级画家 painter.drawImage(rect(),musicImage); } 分析: 这段代码是用于绘制图形视图的部分。 1. void GraphicV…

JavaCard学习笔记: CAP Component 之 Class Component

文章目录 整体结构tag和size字段signature_pool_length和signature_pooltype_descriptor结构导入类型编码导入项签名示例导入类导入数组导入远程方法 interfaces[]interface_info结构flagsinteface_countsuperinterfacesinterface_name class_info_compact classes[]结构flagsi…

mapreduce中的ReduceTask工作机制(Hadoop)

ReduceTask 是 Hadoop 中的一个重要组件,负责对 MapTask 的输出进行合并、排序和归并,最终生成最终的输出结果。 ReduceTask 的工作机制 1. 分组(Shuffle)阶段: 在分组阶段,ReduceTask 会从多个 Mapper …

【问题处理】银河麒麟操作系统实例分享,服务器操作系统VNC远程问题分析

1.服务器环境以及配置 【内核版本】 4.19.90-23.8.v2101.ky10.aarch64 【OS镜像版本】 0518-server 2.问题现象描述 服务器通过vncserver:1.service服务启动的vnc服务后,普通用户用vnc连接时,锁屏后,然后输入登陆密码会报密码错误&…

回溯算法练习day.4

93.复原ip地址 链接:. - 力扣(LeetCode) 题目描述: 有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 . 分隔。 例如:"…

基于单片机的智能病床呼叫系统设计与仿真

摘 要 本文设计的病床呼叫系统采用单片机作为控制器。该系统具有远程控制、病人的身体情况检测、报警呼叫、显示和执行器运动的功能。远程控制由红外线传感器和矩阵键盘组成,检测电路由温湿度传感器DH22、心率传感器Pulse Sensor、压力传感器MPX4115组成&#x…