为什么有深浅拷贝
这得从JavaScript的变量中包含两种类型的值说起
-
基本类型值
基本类型值指的是存储在栈中的一些互相隔离的简单的数据段,比如 String, Number, Boolean 等简单类型
-
引用类型值
引用类型值是引用类型的实例,它是保存在堆内存中的一个对象,引用类型是一种数据结构,最常用的是Object, Array, Function类型,另外还有Date, RegExp, Error等,es6同样也提供了Set, Map新的数据结构
例如:
var obj = {a: 1} var obj1 = obj obj === obj1 // true obj1.a = 2 obj.a // 也变成了2
在实际内存中发生了:
这种情况下,修改了Obj1,就会修改到Obj,于是就有了“拷贝”之说。
1. 浅拷贝
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
也就说浅拷贝是将两个变量进行异化,但未改变内部可能还存在的引用类型。个人总结浅拷贝有以下几种方式,适用于对象和数组:
1.1 遍历拷贝
function cloneByTraverse(obj) {
let newObj = Array.isArray(obj) ? [] : {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
特点:“暴力”枚举第一层属性进行浅拷贝
1.2 Object.assign
function cloneByAssign(obj) {
return Object.assign(Array.isArray(obj) ? [] : {}, obj)
}
特点:代码简洁,易实现,并且:
- 不会拷贝对象继承的属性
- 不可枚举的属性
- 属性的数据属性/访问器属性
- 可以拷贝Symbol类型
1.3 ES6的对象展开
function cloneByExpand(obj) {
return Array.isArray(obj) ? [...obj] : {...obj}
}
特点:代码简洁,易实现,和Object.assign有一样的问题,只支持 e6 及以上的语法
1.4 Array.slice 或 Array.from
这个只针对数组
function cloneBySlice(arr) {
return arr.slice()
}
function cloneByFrom(arr) {
return Array.from(arr)
}
特点:利用了纯净函数不改变参数返回新的内容的特点,事实上也是间接利用了 slice 或 from 中的拷贝,只支持 e6 及以上的语法
2. 深拷贝
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
简单来说就是复制一份完全一样和隔离的对象。深拷贝主要要通过逐层递归遍历去实现了:
2.1 JSON 处理
function cloneByJSON(obj) {
return JSON.parse(JSON.stringify(obj))
}
特点:利用了JSON一进一出,实现拷贝复制,但缺点还是比较多的:
- 拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失
- 无法拷贝不可枚举的属性,无法拷贝对象的原型链
- 拷贝Date引用类型会变成字符串
- 拷贝RegExp引用类型会变成空对象
- 对象中含有NaN、Infinity和-Infinity,则序列化的结果会变成null
- 当对象中存在循环引用,例如obj[key] = obj,会出错
2.2 遍历拷贝
function deepCloneByTraverse(obj) {
let cloneObj = Array.isArray(obj) ? [] : {}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'object') {
//值是对象就再次调用函数
cloneObj[key] = deepCloneByTraverse(obj[key])
} else {
//基本类型直接复制值
cloneObj[key] = obj[key]
}
}
}
return cloneObj
}
特点:逐层扫描,如果是引用类型,则递归,否则直接复制,但还是存在以下缺点:
- 并不能复制不可枚举的属性以及Symbol类型
- 这里只是针对Object引用类型的值做的循环迭代,而对于Array,Date,RegExp,Error,Function引用类型无法正确拷贝
- 对象循环引用成环了的情况
因此,进行优化如下:
function deepClone(obj, hash = new WeakMap()) {
if (obj.constructor === Date)
return new Date(obj) //日期对象就返回一个新的日期对象
if (obj.constructor === RegExp)
return new RegExp(obj) //正则对象就返回一个新的正则对象
//如果成环了,参数obj = obj.loop = 最初的obj 会在WeakMap中找到第一次放入的obj提前返回第一次放入WeakMap的cloneObj
if (hash.has(obj))
return hash.get(obj)
let allDesc = Object.getOwnPropertyDescriptors(obj) //遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc) //继承原型链
hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) { //Reflect.ownKeys(obj)可以拷贝不可枚举属性和符号类型
// 如果值是引用类型(非函数)则递归调用deepClone
cloneObj[key] =
(isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ?
deepClone(obj[key], hash) : obj[key];
}
return cloneObj
}
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
- 利用Reflect.ownKeys()方法,能够遍历对象的不可枚举属性和Symbol类型
- 当参数为Date,RegExp类型则直接生成一个新的实例
- 使用Object.getOwnPropertyDescriptors()获得对象的所有属性对应的特性,结合Object.create()创建一个新对象继承传入原对象的原型链
- 利用WeekMap()类型作为哈希表,WeekMap()因为是弱引用的可以有效的防止内存泄露,作为检测循环引用很有帮助,如果存在循环引用直接返回WeekMap()存储的值