JavaScript中的深拷贝与浅拷贝

在JavaScript中,我们首先要明确拷贝只针对 ObjectArray 等引用数据类型,而 赋值 并不是一种拷贝操作

拷贝操作可以分为 深拷贝浅拷贝,特别是对于多层级的数组或者对象,不同的拷贝方式会产生完全不同的结果

赋值

在JavaScript中当我们把一个对象赋值给一个新的变量时,只是将栈空间中存储的对象地址复制了一份给新的变量,新的变量和对象依旧指向同一个堆中的存储空间,对新变量的任何操作都会作用在原来的对象上

JavaScript 复制代码
let arr = [1,2,3]
let newArr = arr//这里仅仅是把数组的内存地址赋值给newArr,这里不叫拷贝

这样的操作并不能叫做 拷贝

浅拷贝

赋值 不同,对于对象或数组,进行 浅拷贝 操作时,会在堆空间中生成一个新的对象,这个对象会精准复制原对象的的所有属性值,并将新的变量指向这个堆中新产生的对象

JavaScript 复制代码
let arr = [1,2,3]
let newArr =[...arr]//这里使用扩展运算符实现浅拷贝

这样即使新数组的数据发生改变也不会影响原数组,我们可以使用 展开运算符, Object.assign(), Array.prototype.slice(), Array.prototype.concat()等方法实现对对象和数组的浅拷贝

JavaScript 复制代码
let obj = {
    name: 'Jerry'
}
let arr = [1,2,3]

//Object的assign方法将多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象
let obj1 = Object.assign({},obj)

//展开运算符实现浅拷贝
let arr1 = [...arr]
let obj1 = {...obj}

//Array.prototype.concat()用于合并两个或多个数组,并返回新的数组
let arr2 = arr.concat([])

//Array.prototype.slice()回一个新的数组对象,这一对象是一个由 start 和 end 决定的原数组的浅拷贝(包括 start,不包括 end)
let arr3 = arr.slice()

浅拷贝 就真的完美了吗? 当然不是,针对单层的 对象 或者 数组 浅拷贝复制出的新数据是完全独立于旧数据的,但是当 对象 的属性和 数组 的元素也是引用数据类型时,浅拷贝 仍然只会复制该引用数据类型在堆中的地址,举个例子:

JavaScript 复制代码
let arr = [1,2,[3]]
let obj = {
    name: 'Jerry'
   info: {
        age: 18
        height: 180
    }
}
let arr1 = [...arr]
let obj1 = {...obj}
arr1[2] = [3,4]
obj1.info = 'null'
console.log(arr[2])//[3,4]
console.log(obj.info)//'null'

当出现引用数据类型的嵌套时,修改 浅拷贝 复制出来的新数据中的引用数据类型部分,依然会作用于原数据

深拷贝

针对 浅拷贝 中存在的问题,深拷贝 在构造新的数据时,遇到引用所指向的引用数据类型会继续执行拷贝,直到所有的引用数据类型都被处理

接下来我们介绍一下常用的实现 深拷贝 的方法:

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

JSON.stringify()将一个 JavaScript 对象或值转换为 JSON 字符串, 而 JSON.parse() 则正好相反,它将JSON字符串解析为值或对象,我们以JSON字符串作为桥梁,来实现 深拷贝

JavaScript 复制代码
let arr = [1,2,[3]]
let obj = {
    name: 'Jerry',
   info: {
        age: 18,
        height: 180
    }
}
let arr1 = JSON.parse(JSON.stringify(arr))
let obj1 = JSON.parse(JSON.stringify(obj))

是不是很简单? 但这样的方法本身有很多缺陷,因为 JSON.stringify() 只能序列化对象的可枚举的自有属性

如果obj里面存在时间对象,JSON.parse(JSON.stringify(obj))之后,时间对象变成了字符串

如果obj里有RegExpError对象,则序列化的结果将只得到空对象

如果obj里有函数undefined,则序列化的结果会把函数, undefined丢失

如果obj里有NaNInfinity-Infinity,则序列化的结果会变成null

如果obj中的对象是由构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的 constructor

如果对象中存在循环引用的情况也无法正确实现深拷贝

2. 借助第三方库

不管是 Lodash 还是 JQuery 中都有非常完善的深拷贝方法可以使用

JavaScript 复制代码
//需要引入对应的库
let arr = [1,2,[3,4]]
let arr1 = ._cloneDeep(arr)//Lodash深拷贝
let arr2 = $.extend(true,[],arr)//jq的extend方法第一个参数为true时即为深拷贝,fasle为浅拷贝

3. 自己手写一个

实现一个基于递归的 深拷贝 方法

JavaScript 复制代码
function deepClone(obj, cache = new WeakMap()) {//用weakmap做缓存处理循环引用,且不影响垃圾回收
  if (typeof obj === 'symbol') return Symbol(obj.description)//处理symbol
  if (obj === null || typeof obj !== 'object') return obj //对于空对象,函数或者基本数据类型直接返回
  if (cache.get(obj)) return cache.get(obj) //如果缓存中有该对象了,说明有循环引用,直接返回该对象,不重复拷贝
  //当为以下类型时直接new一个新的
  const type = [Date, RegExp, Set, Map, WeakMap, WeakSet]
  if (type.includes(obj.constructor)) return new obj.constructor(obj)
  let cloneObj = new obj.constructor()//让拷贝对象拥有原对象的构造函数类型
  cache.set(obj,cloneObj)//拷贝对象写入缓存
  for(let key in obj) {
    if(obj.hasOwnProperty(key)){//如果该属性在对象上而不是其原型上
      cloneObj[key] = deepClone(obj[key],cache)//递归调用
    }
  }
  return cloneObj

}

上述的实现解决了循环引用,并处理了常见的数据类型,但是依然可以继续完善,比如处理原型链的问题等

相关推荐
腾讯TNTWeb前端团队6 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom11 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom11 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom11 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom11 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom11 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试