Skip to main content

深拷贝

之前写的文章 - JavaScript知识之 浅拷贝与深拷贝

引言

对于基本数据类型和引用数据类型的复制方法是与不一样的,这里复习以下 JavaScript 的数据类型:

  • 基本数据类型:StringNumberBooleanNullUndefinedSymbol(ES6引入)
  • 引用数据类型:ObjectArrayFunctionRegExpDate...)

浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,并且在修改新对象不会影响原对象。

浅拷贝

浅拷贝的几种方法:

  • 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 // {}

可以发现,目前只对普通objectarray进行了拷贝,遇到 SetRegExp 等对象时并没有正确复制。 需要分类进行拷贝,使用 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;
}

参考文章