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

}

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

相关推荐
lfl183261621601 分钟前
thingsboard edge 在windows 环境下的配置
前端·edge
IT、木易7 分钟前
大白话react第十五章React 应用性能优化深度实践
前端·react.js·前端框架
十八朵郁金香8 分钟前
分享react后台管理系统常见的组件/知识点
前端·react.js·前端框架
指尖时光.10 分钟前
【前端进阶】15 提升开发效率 数据抽象与配置化
前端
呼啦啦呼_14 分钟前
Echarts map 点击地图某个区域上色
前端
凌览25 分钟前
2.3k Star!免费又好用的图片压缩神器,1 秒瘦身不模糊!
前端·后端·面试
MiyueFE1 小时前
bpmn-js 源码篇7:Featrues 体验优化与功能扩展(二)
前端
好_快1 小时前
Lodash源码阅读-isPrototype
前端·javascript·源码阅读
31535669131 小时前
manus邀请码申请手把手教程
前端·后端·面试
烂蜻蜓2 小时前
HTML 编辑器推荐与 VS Code 使用教程
前端·python·编辑器·html