JavaScript中的深拷贝与浅拷贝

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

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

赋值

在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里有RegExp、Error对象,则序列化的结果将只得到空对象

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

如果obj里有NaN、Infinity和-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

}

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

相关推荐
半个落月6 小时前
LangSmith 全链路观测(下):用 Dataset 与 LLM Judge 量化评估 RA
javascript·langchain
半个落月7 小时前
LangSmith 全链路观测(上):用 LangGraph 与 Milvus 构建可追踪的 RAG 客服
javascript·langchain
Java后端的Ai之路7 小时前
06-React-Props
前端·react.js·前端框架
半个落月7 小时前
LangChain.js Agent 中间件实战:生命周期、动态工具与文件权限
javascript·langchain
Flynt8 小时前
开源"间谍卫星"说数据全是真的,我把它的免key数据源挨个试了一遍
前端·开源·数据可视化
Csvn8 小时前
渲染管线
前端
程序员大阳9 小时前
副队长CSS教程(33)–圆角与透明
前端·css·透明·圆角
志尊宝9 小时前
Vue3 零基础每日笔记(082):文件上传实战——el-upload 校验、自定义上传、头像预览
前端·javascript·vue.js·笔记·#javascript·#vue.js
鬼手点金9 小时前
opencode-rules 插件
java·开发语言·前端·人工智能·chrome·git·github
柳杉9 小时前
借助 Codex 和可视化工作流,从调研到完整实现,3D 大屏竟然可以这么简单
前端·ai编程·数据可视化