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

}

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

相关推荐
.生产的驴2 分钟前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
awonw5 分钟前
[前端][easyui]easyui select 默认值
前端·javascript·easyui
老齐谈电商7 分钟前
Electron桌面应用打包现有的vue项目
javascript·vue.js·electron
九圣残炎26 分钟前
【Vue】vue-admin-template项目搭建
前端·vue.js·arcgis
柏箱1 小时前
使用JavaScript写一个网页端的四则运算器
前端·javascript·css
TU^1 小时前
C语言习题~day16
c语言·前端·算法
一颗花生米。4 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐014 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19954 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式