首页 > 其他分享 >八股文: 讲讲什么是浅拷贝、深拷贝?

八股文: 讲讲什么是浅拷贝、深拷贝?

时间:2024-10-30 09:47:09浏览次数:3  
标签:const name 讲讲 age obj 八股文 拷贝 内存

引言

说起 浅拷贝深拷贝 可以说是面试中经常碰到的经典问题, 并且在实际项目开发过程中, 也常常会因为数据拷贝问题, 导致一些隐藏的 BUG

javascript 中有很多方法能够复制对象, 但是如果你对数据拷贝不是很了解, 在复制对象时就会很容易掉进陷阱里, 那么我们怎样才能正确地复制一个对象呢, 本文将会慢慢进行揭秘

一、前置知识

本节将对 JS 中数据存储方式进行简单介绍, 这里提到的是目前网上普遍的一个看法, 实际上 JS 中数据到底如何存储还是存在争议的,这里就不作深究, 后续如果有机会再进行详细的讲解。

1.1 数据分类

如下图所示, JS 中数据类型大体可划分为, 基本数据引用数据 两大类

image

1.2「基本数据」存储方式

JS基本数据 是存储在 栈内存(Stack Memory) 中, 它们的值是直接存储在变量访问的位置

那么什么是 栈内存 呢? 它是一种计算机内存中划分出来的一块 连续存储区域, 它的主要特点是 先进后出

当我们创建一个 基本数据 的变量时, 因为它占用空间小、大小固定, 所以会在 栈内存 中分配一个固定大小的空间来存储这个值, 当这个变量不再被使用时, 它所占用的空间会被自动释放, 因此 基本数据 的赋值和拷贝操作非常快速和高效

创建值: 下面是演示代码以及 栈内存 信息展示, 代码中创建了 3 个变量, 对应的 栈内存 中也开辟了 3 块空间用于存储数据

const name = 'moyuanjun'
const age = 18
const address = '杭州'

image

修改值: 直接根据变量找到 栈内存 中对应值进行修改即可, 下面是演示代码以及对应的 栈内存 修改前后的的变更

const name = 'moyuanjun'
let age = 18
const address = '杭州'

// 修改值
age = 20

image

复制值: 新开辟一个空间, 根据变量找到 栈内存 中对应值, 拷贝一份新的值, 下面是演示代码以及对应的 栈内存 信息, 代码中对 age 进行了拷贝, 同时修改了 age, 会发现只有变量 age 发生变更, 因为 ageage2 的存储是独立的两个空间

const name = 'moyuanjun'
let age = 18
const address = '杭州'

// 拷贝一份
const age2 = age

// 修改值
age = 20

image

1.3「引用数据」存储方式

JS引用数据 是存储在 堆内存(Heap Memory) 中的, 因为它们的大小是不确定的, 对象的属性和方法可能会动态增加或删除

那么什么是 堆内存 呢? 它是一种计算机内存中划分出来的一块 非连续存储区域, 它的特点是可以动态分配和释放内存空间, 但它需要手动管理内存空间

当我们创建一个 引用数据, 会在 堆内存 中分配一个内存空间用来存储对象的所有属性和方法, 然后在 栈内存 中创建一个指向该内存空间的 指针, 这个指针存储在变量访问的位置, 当这个变量不再被使用时,栈内存 中的指针被销毁, 但 堆内存 中的对象空间不会自动释放, 需要手动调用 垃圾回收机制 来释放这些空间

如下代码声明了两个 基本数据 分别是 nameage, 它们将被直接存储在 栈内存 中, 同时还声明了 引用数据 user, 它会被分两部分进行存储, 实体部分会被存储在 堆内存 中, 在 栈内存 中将存储着 实体 的地址

const name = 'moyuanjun'
const age = 18
const user = { age: 18 }

image

上面例子中, 当我们访问 引用数据 时, 会先查找 栈内存 找到实体在 堆内存 中的地址, 取得地址后在 堆内存 中获得实体

修改变量为 基本数据: 当我们修改变量的值时, 实际上是修改了 栈内存 中的 引用地址, 下面是演示代码、以及对应内存修改前后的状态

const name = 'moyuanjun'
const age = 18
let user = { age: 18 }
user = 'lh'

image

修改变量(重新赋一个对象): 当我们修改变量的值时, 实际上是修改了 栈内存 中的 引用地址, 下面是演示代码、以及对应内存修改前后的状态

const name = 'moyuanjun'
const age = 18
let user = { age: 18 }
uuser = { name: 'lh' }

image

修改对象属性: 先从 栈内存 找到实体的 引用地址, 然后再根据 引用地址 找到实例, 再对 实体 进行修改, 下面是演示代码、以及修改前后内存状态

const name = 'moyuanjun'
const age = 18
const user = { age: 18 }

user.age = 20
user.name = 'lh'

image

注意: 在 JS 中对于 原始类型 的拷贝是直接复制数据的, 并没有深浅拷贝的区别, 我们讨论的深浅拷贝都只针对 引用数据

二、赋值

首先我们需要先区分一下赋值操作和拷贝的区别, 赋值 是将一个变量 A 赋给另一个变量 B

  1. 对于 基本数据 来说, 就是完全的复制了一份新值进行赋值
  2. 对于引用数据则是将 A引用地址 拷贝了一份给了 B, 但他们公用的是一个实体

下面是 基本数据、和 引用数据 赋值的演示代码、以及赋值后内存的情况, 代码中将 基本数据 赋给了对象的属性, 作为属性值进行使用

const age =18
const name = 'moyuanjun'

const user1 = { name, age }
const user2 = user1

image

赋值, 在我理解上也算是 拷贝, 是对变量值的拷贝; 但我们今天要讲的是对 引用数据 的拷贝, 在这儿就有了 浅拷贝深拷贝 的区分了

三、深拷贝

深拷贝: 创建一个 新对象, 拷贝对象的所有属性, 如果属性是 基本数据, 拷贝的就是 基本数据 的值; 如果是 引用数据, 则需要重新分配一块内存, 拷贝该 引用数据 的所有属性, 然后将 引用地址 赋值给对应的属性, 如果该 引用数据 中某个属性也是 引用数据 则需要继续一层层递归拷贝……

简单来说: 深拷贝 就是完整的拷贝了一份一模一样结构的数据, 拷贝后的数据和源数据是没有任何关联的, 修改原数据不会修改到拷贝后的数据

image

3.1 手写深拷贝

通过手写一个 深拷贝 方法, 来更深入了解 深拷贝, 总体思路如下:

  1. 目标类型判断, 如果是非 引用数据 则直接返回
  2. 针对特殊的 引用数据 进行单独处理
  3. 判断当前拷贝的目标数据, 是否已经拷贝过, 如果拷贝过则返回上次拷贝的数据: 目的是解决 共同引用循环引用 等问题
  4. 新建一个对象
  5. 循环对象的所有属性, 如果属性值是个 基本数据, 则直接返回该值, 如果属性值是个 引用数据, 则需要递归调用(新建对象、拷贝属性……)
// map 用于记录出现过的对象, 解决循环引用
const deepClone = (target, map = new WeakMap()) => {
  // 1. 对于基本数据类型(string、number、boolean……), 直接返回
  if (typeof target !== 'object' || target === null) {
    return target
  }

  // 2. 函数 正则 日期 MAP Set: 执行对应构造题, 返回新的对象
  const constructor = target.constructor
  if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) {
    return new constructor(target)
  }

  // 3. 解决 共同引用 循环引用等问题
  // 借用 `WeakMap` 来记录每次复制过的对象, 在递归过程中, 如果遇到已经复制过的对象, 则直接使用上次拷贝的对象, 不重新拷贝
  if (map.get(target)) {
    return map.get(target)
  }

  // 4. 创建新对象
  const cloneTarget = Array.isArray(target) ? [] : {}
  map.set(target, cloneTarget)

  // 5. 循环 + 递归处理
  Object.keys(target).forEach(key => {
    cloneTarget[key] = deepClone(target[key], map);
  })

  // 6. 返回最终结果
  return cloneTarget
}

3.2 JSON.parse(JSON.stringify())

这里利用 JSON.stringify 将对象转成 JSON 字符串, 再用 JSON.parse 把字符串解析成对象, 如此一来一去就能够实现 引用数据 的一个深拷贝

const obj = {
  age: 18,
  name: 'moyuanjun',
}

const res = JSON.parse(JSON.stringify(obj))

注意该方法的 6 个局限性: 因为 JSON 不是 JS 独有的数据格式, 所以 JSON.stringify 需要抹平和其他语言的差异

  1. NaN Infinity -Infinity 会被序列化为 null
  2. Symbol undefined function 会被忽略(对应属性会丢失)
  3. Date 将得到的是一个字符串
  4. 拷贝 RegExp Error 对象,得到的是空对象 {}
const obj = {
  num1: NaN,
  num2: Infinity,
  num3: -Infinity,

  symbol: Symbol('xxx'),
  name: undefined,
  add: function(){},

  date: new Date(),

  reg: /a/ig,
  error: new Error('错误信息')
}

console.log(JSON.parse(JSON.stringify(obj)))
// 打印结果
// {
//   num1: null,
//   num2: null,
//   num3: null,
//   date: '2023-03-03T03:40:38.594Z',
//   reg: {},
//   error: {}
// }
  1. 多个属性如果复用同一个 引用数据 A 时, 拷贝的结果和原数据结构不一致(会完整拷贝多个 引用数据 A), 如下代码所示: 对象 objbasechildren 指向同一个对象, 但是 JSON.parse(JSON.stringify()) 复制出来的对象 resbasechildren 指向了不同的对象, 也就是说拷贝后的 res 对象和原对象 obj 数据结构不一致
const base = {
  name: '张三',
  age: 18,
}

const obj = {
  base,
  children: base
}

const res = JSON.parse(JSON.stringify(obj))

// 原对象, obj.base obj.children 指向同一个对象
obj.base.name = '李四'
console.log(obj.base === obj.children) // true
console.log(obj.children.name) // 李四 

// 拷贝后, res.base res.children 指向了不同对象, 拷贝了两个(数据结构被改了)
res.base.name = '李四'
console.log(res.base === res.children) // false
console.log(res.children.name) // 张三 

下图是对象 obj 和拷贝后对象 res 的内存结构图

image

  1. 在存在 循环引用 的对象中使用将会报错

使用 JSON.stringify() 序列化循环引用的对象, 将会抛出错误

const base = {
  name: '张三',
  age: 18,
}

base.base = base

// TypeError: Converting circular structure to JSON
const res = JSON.parse(JSON.stringify(base))

更对细节可参考 MDN

3.3 使用 structuredClone

structuredClone 是一个新的 API 可用于对数据进行 深拷贝, 同时还支持循环引用

const base = {
  name: '张三',
  age: 18,
}

const obj = { base }

obj.obj = obj

const res = structuredClone(obj) 

注意: 使用 structuredClone 进行拷贝, 如果有个属性值是个函数, 方法会抛出错误

// DOMException [DataCloneError]: () => {} could not be cloned.
const res = structuredClone({
  add: () => {}
})

有关 structuredClone 更多信息查看 MDN

3.4 使用第三方库

可以使用一些第三方库提供的工具方法来实现拷贝, 比如: lodash 中的 cloneDeep 方法

const base = {
  name: '张三',
  age: 18,
}

const obj = { base }

obj.obj = obj
const res = _.cloneDeep(obj)

四、浅拷贝

浅拷贝: 会新建一个对象, 拷贝对象的所有属性值, 对于 基本数据 来说就是拷贝一份对应的值, 但是对于 引用数据 则是拷贝一份 引用数据 的引用地址

image

4.1 手写浅拷贝

通过手写一个浅拷贝方法, 来更深入了解 浅拷贝, 总体思路如下:

  1. 如果拷贝对象是个 基本数据, 则直接返回该值
  2. 新建一个对象
  3. 循环对象的所有属性, 并拷贝属性值, 如果该属性是 引用s数据 拷贝的则是数据的引用地址
const clone = (target) => {
  // 1. 对于基本数据类型(string、number、boolean……), 直接返回
  if (typeof target !== 'object' || target === null) {
    return target
  }

  // 2. 创建新对象
  const cloneTarget = Array.isArray(target) ? [] : {}

  // 3. 循环 + 递归处理
  Object.keys(target).forEach(key => {
    cloneTarget[key] = target[key];
  })

  return cloneTarget
}
const res= clone({ name: 1, user: { age: 18 } }) 

4.2 Object.assign()

Object.assign(target, ...sources) 方法将 sources 中所有的源对象的可枚举属性复制到目标对象 target 中, 最后返回修改后的 target 对象

const address = ['杭州']

const base1 = {
  age: 18,
  name: 'lh',
}

const base2 = {
  age: 20,
  address,
  name: 'moyuanjun',
}

// 将 base1 base2 中的属性添加到, 对象 {} 中
// base1 base2 存在相同属性, 会被 base2 的覆盖掉
const res = Object.assign({}, base1, base2)

res.address === address // true
res.address === base2.address // true

image

关于 Object.assign() 更多细节参考 MDN

4.3 展开运算符 ...

展开运算符 ..., 可以数组或对象在语法层面展开, 从而实现数组或对象的一个浅拷贝

const address = ['杭州']

const base1 = {
  age: 18,
  name: 'lh',
}

const base2 = {
  age: 20,
  address,
  name: 'moyuanjun',
}

const res = { ...base1, ...base2 }

res.address === address // true
res.address === base2.address // true

关于 展开运算符 更多细节参考 MDN

4.4 数组方法

对于数组可以使用, 数组的一些方法进行拷贝, 比如: Array.prototype.concat() Array.prototype.slice() Array.from 等方法, 它们的特点都是不改变原数组、同时返回一个新的数组

const base = {
  age: 18,
  name: 'lh',
}

const arr = [1, 'moyuanjun', base]

arr.concat([])
arr.slice()
Array.from(arr)

4.5 第三方库

可以使用一些第三方库提供的工具方法来实现拷贝, 比如: lodash 中的 clone 方法

const base = {
  name: '张三',
  age: 18,
}

const obj = {
  base,
  address: ['杭州'],
}

const res = _.clone(obj)

五、是浅拷贝还是深拷贝

如下代码: 当一个 引用数据 中所有属性都是 基本数据, 那么对它使用上文提到的浅拷贝方法对它进行了拷贝

const obj = {
  age: 18,
  name: 'lh',
  address: '杭州',
}

const res = {...obj}

image

请问上面例子是 浅拷贝 还是 深拷贝 呢? 这里个人看法是 深拷贝, 因为从结果来看 objres 从内存、数据结构上来看是两个完全独立、毫不相干的, 并且对 obj 进行操作也都不会影响到 res (当然你如果操作 Object.prototype 那另当别论)

六、总结

| 操作 | 基本类型 | 引用数据 | 结果 |
|—|—|–|
| 赋值 | 重新创建值 | 复制引用地址 | 具有相同的变量、属性值 |
| 深拷贝 | 重新创建值 | 递归遍历, 拷贝所以属性 | 拷贝对象和源对象完成隔离 |
| 浅拷贝 | 重新创建值 | 复制引用地址 | 拷贝对象和源对象存在共同的引用对象 |

七、参考


image

标签:const,name,讲讲,age,obj,八股文,拷贝,内存
From: https://blog.csdn.net/qianyin925/article/details/143357277

相关文章

  • 2024年10月新版Java面试八股文大总结!
    线程有哪几种状态。(1)NEW线程至今尚未启动(2)RUNNABLE线程正在 Java虚拟机中执行(3)BLOCKED受阻塞并等待获得同步代码块的锁(4)WAITING无限期地等待另一个线程来执行某一特定操作(5)TIMED_WAITING在指定的时间内等待另一个线程来执行某一特定操作(6)TERMINATED线程已退出注......
  • 2024Java八股文(面试必备)
    1封装的目的是什么,为什么要有封装?封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以实现以下目的:隐藏类的实现细节;限制对成员变量的不合理访问;提高代码的可维护性。2说......
  • 最新Java后端面试八股文汇总!
    1.为什么Java语言不支持多重继承?为了程序的结构能够更加清晰从而便于维护。假设Java语言支持多重继承,类C继承自类A和类B,如果类A和B都有自定义的成员方法f(),那么当代码中调用类C的f()会产生二义性。Java语言通过实现多个接口间接支持多重继承,接口由于只包含方法定义,不能有方法......
  • 2024最新互联网一线大厂最新高质量 Java 面试八股文汇总(附答案)
    最近很多粉丝朋友私信我说:熬过了去年的寒冬却没熬过现在的内卷;打开Boss直拒一排已读不回,回的基本都是外包,薪资还给的不高,对技术水平要求也远超从前;感觉Java一个初中级岗位有上千人同时竞争,内卷程度简直怀疑人生。事实也确实是这样:我国大概有400-700万程序员,其中光Java......
  • 【探讨Python中的浅拷贝与深拷贝】如何避免共享引用带来的问题!
    探讨Python中的浅拷贝与深拷贝:如何避免共享引用带来的问题在Python编程中,拷贝(Copy)是一个常见的操作,尤其在数据处理、对象传递等情况下,经常会涉及数据的复制操作。浅拷贝和深拷贝的概念对于了解如何复制对象而不影响原始对象至关重要。本文将深入讨论这两种拷贝的原理、区别......
  • 易考八股文之Redis在你项目中怎么用,如果Redis宕机,应用服务还会响应吗?会造成哪些问题,如
    在项目中,Redis可以用于多种用途,例如:缓存数据:将经常访问的数据存储在Redis中,减少对后端数据库的查询压力,提高应用的响应速度。会话管理:存储用户会话信息,方便在分布式系统中管理用户登录状态等。如果Redis宕机,应用服务可能仍然会响应,但会面临一些问题:数据丢失:如果没有配置持久......
  • 浅拷贝与深拷贝 以及嵌套和递归使用模板类
     1.浅拷贝和深拷贝——对象和类浅拷贝浅拷贝只复制对象的基本属性,而不复制引用类型的属性指向的对象,即只复制引用而不复制引用指向的对象在浅拷贝中,新对象与原始对象共享引用类型的属性指向的对象,即它们指向同一个对象。编译器提供的拷贝构造函数是浅拷贝,当一个对象修......
  • 2024最新互联网工程师 Java 面试八股文及答案整理
    2024金九银十即将结束,竟很多同学会问Java面试八股文有必要背吗?!!我的回答是:很有必要!!!!你可以讨厌这种模式,但你一定要去背,因为不背你就进不了大厂。国内的互联网面试,恐怕是现存的、最接近科举考试的制度。而且,我国的八股文确实是独树一帜。以美国为例,北美工程师面试比较重视算......
  • 揭秘!“八股文”在职场中的真实角色,是助力还是阻力?
    “八股文”在实际工作中既有其助力的一面,也存在阻力和空谈的风险。下面将从多个角度详细分析“八股文”的作用:基础知识的掌握:“八股文”通常涉及编程语言基础、数据结构与算法、系统设计等知识点。通过提问这些基础知识,面试官可以快速评估候选人的技术基础是否扎实。扎实的基......
  • 浅拷贝与深拷贝
    引言       在编程中,深拷贝和浅拷贝是两种不同的对象复制方法,它们的主要区别在于复制的对象是否包含对其它对象的引用,以及这些引用是否也被复制。一、浅拷贝        浅拷贝是指创建一个新的对象,这个新对象与原对象具有相同的属性值,但如果属性值是对其它对......