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

}

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

相关推荐
Martin -Tang17 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发18 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html