在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
}
上述的实现解决了循环引用,并处理了常见的数据类型,但是依然可以继续完善,比如处理原型链的问题等