深拷贝
之前写的文章 - JavaScript知识之 浅拷贝与深拷贝
引言
对于基本数据类型和引用数据类型的复制方法是与不一样的,这里复习以下 JavaScript
的数据类型:
- 基本数据类型:
String
、Number
、Boolean
、Null
、Undefined
、Symbol
(ES6引入) - 引用数据类型:
Object
(Array
、Function
、RegExp
、Date
...)
浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,并且在修改新对象不会影响原对象。
浅拷贝
浅拷贝的几种方法:
Array.prototype.slice()
Object.assign()
Array.prototype.concat()
- ES6拓展运算符
...
深拷贝
深拷贝的几种方法:
JSON.parse(JSON.stringify())
_.cloneDeep()
- lodash库中的方法
jQuery.extend()
- jQuery中的方法
- 循环递归
JSON.parse(JSON.stringify())
方法并不适合所有的深拷贝,并不能完整的克隆所有的类型。而 _.cloneDeep()
和 jQuery.extend()
是第三方库的实现,为了一个方法而引入一个库并不合适,所以这里用原生 js 实现深拷贝
想法:建一个新对象,然后把需要被克隆对象的每一个值都复制给新对象。如果是基本数据类型就直接复制,如果是引用类型就调用递归。
简单版本:
const deepClone = (obj) => {
if(typeof obj !== 'object') {
return obj;
}
const newObj = {};
for(const key in obj) {
newObj[key] = deepClone(obj[key]);
}
return newObj;
}
兼容数组
const deepClone = (obj) => {
if(typeof obj !== 'object') {
return obj;
}
const newObj = Object.prototype.toString.call(obj) === "[object Array]" ? [] : {};
for(const key in obj) {
newObj[key] = deepClone(obj[key]);
}
return newObj;
}
自引用情况
由于直接和间接引用了自身,在克隆对象的时候,就不断的循环创建一块内存地址来存放数据,导致堆栈溢出。
解决方案:将访问过的(复制过的)对象保存起来,复制前先查询是否复制过,如果复制过就拿出来用。这里使用 WeakMap
对象,其是弱引用的,性能会更好。
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收
const deepClone = (obj, map = new WeakMap()) => {
if(typeof obj !== 'object') { // 基本数据类型,直接返回即可
return obj;
}
if(obj === null) { // WeakMap 不能用 null 做 key
return null;
}
const newObj = Object.prototype.toString.call(obj) === "[object Array]" ? [] : {}; // 兼容数组
if(map.get(obj)) { // 如果复制过,直接返回
return map.get(obj);
}
map.set(obj, newObj); // 保存复制过的;这个一定要在递归调用之前!!!
for(const key in obj) {
newObj[key] = deepClone(obj[key], map); // 递归复制
}
return newObj;
}
兼容其它数据类型
测试上面写好的 深复制函数 :
const obj = { num: 10, str: "obj", bool: true, node: null, root: undefined, data: [1,2,3,4, { num: 20 }], set: new Set([1,2,3]), sym: Symbol(), fun: function fun() {}, innerObj: { fuc: function fuc() {}, num: 12, str: 'inner' }, time: new Date(), regexp: /12/g, self: null}obj.self = objconst a = deepClone(obj)a.set // {}a.regexp // {}
可以发现,目前只对普通object
和array
进行了拷贝,遇到 Set
和 RegExp
等对象时并没有正确复制。
需要分类进行拷贝,使用 Object.prototype.toString.call
进行类型判断。
写个函数,判断类型:
// 获取数据类型
function getType(obj) {
// '[object Boolean]' | '[object Number]' | '[object String]'
// '[object Array]' | '[object Function]'
// '[object Error]' | '[object RegExp]' | '[object Date]'
// '[object Object]' ......
return Object.prototype.toString.call(obj);
}
其实有很多类型:
// 可遍历的类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';
// 不可遍历的类型
const booleanTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
// ......
上述判断数据类型,出现可遍历属性和不可遍历属性,主要是可遍历属性我们需要用到这些对象原型prototype方法和构造函数constructor,需要遍历这些对象原型上和构造函数上的方法。 下面我们就用 constructor这种方式来获取。
function getInit(obj) {
const proto = obj.constructor;
return new proto();
}
最终版本
其实还有很多类型没有考虑到!
实际上 lodash
库就是使用的类似方法,该库所有细节都考虑到了,可以参考他们的源码:lodash
// 可遍历类型
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const mapTag = '[object Map]';
const setTag = '[object Set]';
const argsTag = '[object Arguments]';
// 可遍历类型数据标识
const deepTagList = [ mapTag, setTag, arrayTag, objectTag, argsTag ];
// 不可遍历类型
const numberTag = '[object Number]';
const stringTag = '[object String]';
const booleanTag = '[object Boolean]';
const dateTag = '[object Date]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
// ---- WeakMap,WeakSet ....
// 获取数据类型
function getType(obj) {
// '[object Boolean]' | '[object Number]' | '[object String]'
// '[object Array]' | '[object Arguments]' | '[object Function]'
// '[object Error]' | '[object RegExp]' | '[object Date]'
// '[object Object]'
return Object.prototype.toString.call(obj);
}
// 初始类型
function getInit(obj) {
const proto = obj.constructor;
return new proto();
}
// 克隆正则
function deepCloneReg(obj) {
const data = new obj.constructor(obj.source, /\w*$/.exec(targe));
data.lastIndex = obj.lastIndex;
return data;
}
// 其它类型
function otherType(data, type) {
const NewCtor = data.constructor;
switch (type) {
case booleanTag:
return new Boolean(data);
case numberTag:
return new Number(data);
case stringTag:
return new String(data);
case errorTag:
return new Error(data);
case symbolTag:
return new Symbol(data);
case dateTag:
return new NewCtor(data);
case regexpTag:
return deepCloneReg(data);
// case ... ...
default:
return null;
}
}
const deepClone = (obj, map = new WeakMap()) => {
if(typeof obj !== 'object' || !obj) { // 非对象类型,直接返回即可
return obj;
}
let newObj = null;
const type = getType(obj);
if(deepTagList.includes(type)) {
newObj = getInit(obj);
} else {
return otherType(obj, type);
}
// map
if(type === mapTag) {
obj.forEach((val, idx) => {
newObj.set(idx, deepClone(val))
})
return newObj;
}
// set
if(type === setTag) {
obj.forEach((val, idx) => {
newObj.add(idx, deepClone(val))
})
return newObj;
}
newObj = Object.prototype.toString.call(obj) === "[object Array]" ? [] : {}; // 兼容数组
if(map.get(obj)) { // 如果复制过,直接返回
return map.get(obj);
}
map.set(obj, newObj); // 保存复制过的;这个一定要在递归调用之前!!!
for(const key in obj) {
newObj[key] = deepClone(obj[key], map); // 递归复制
}
return newObj;
}