作为一个前端,一定对深拷贝这个名词不陌生,深拷贝是非常重要的。

为什么存在深拷贝?

深拷贝这个名词的诞生也是有由来的,在JS里,所有的变量类型都可以简单分为基本类型和应用类型。

比如 ​​123, 'aaa'​​​ 数值和字符串,属于基本类型,它们在进行赋值的时候,可以直接赋值,比如​​let a = 123, b = a;​​​这个是没有问题的,​​a​​​和​​b​​互不影响。

但是​​[1, 2, 3] { a: 1 }​​​,像数组和对象这种引用类型,在进行赋值拷贝的时候,简单的​​=​​并不足够,如果你依然像对待基本类型一样对待引用类型的拷贝。你拷贝的只是一个单纯的内存地址,他们会互相影响。

基本类型和引用类型的关系,大家可以自己找资料学习,这里不过多赘述,反正要知道一点,深拷贝诞生是因为JS存在引用类型的数据导致的。

在前端的面试中,手写深拷贝的问题出现率非常之高。在一些大厂中,还要让你考虑很多的情况,比如循环引用之类的。

什么是循环引用?

比如

let obj1 = {};
let obj2 = {};
obj1.obj2 = obj2;
obj2.obj1 = obj1;

这样就会造成对象间的循环引用,这个也需要考虑在深拷贝的实现之中。

JS手写实现深拷贝(考虑循环引用)_前端

手写实现深拷贝(不考虑循环引用)

我们先实现一个最基本的深拷贝版本,不考虑循环应用。

function deepClone(obj, newObj) {
newObj = newObj || {};

for (let i in obj) {
if (typeof obj[i] === "object") {
newObj[i] =
Object.prototype.toString.call(obj[i]) === "[object Array]" ? [] : {};
deepClone(obj[i], newObj[i]);
} else {
newObj[i] = obj[i];
}
}
}

最基本的思路是依赖递归来实现。

对需要深拷贝的​​obj​​​变量进行​​for in​​​循环,判断每个属性的类型。我这里使用的是​​Object.prototype.toString​​方法来判断变量类型,也可以用其它方法。

如果当前循环的属性是基本类型,直接进行 ​​=​​ 赋值,如果是引用类型,就进行递归,直到当前属性递归到基本类型,再进行赋值。

其实还是比较简单的。

我们来验证一下,是否实现了深拷贝。

let obj = {
name: "微信公众号: Code程序人生",
age: 22,
sex: "男",
hobby: ["跑步", "读书", "睡觉"],
fn: function () {
console.log(this.name);
},
};

let newObj = {};

deepClone(obj, newObj);

obj.name = 'CreatorRay1';
obj.hobby[0] = '运动';

console.log('newObj', newObj);
console.log('obj', obj);

JS手写实现深拷贝(考虑循环引用)_前端_02

通过结果可以看到,新创建的​​newObj​​对象跟​​obj​​对象互不联系,在修改某一方的值时,另一方并不受到影响,所以它们的内存地址是不一样的。

如果我们直接 ​​=​​ 赋值,会是什么样子?

let obj = {
name: "微信公众号: Code程序人生",
age: 22,
sex: "男",
hobby: ["跑步", "读书", "睡觉"],
fn: function () {
console.log(this.name);
},
};

let newObj = {};

// deepClone(obj, newObj);

newObj = obj;

obj.name = 'CreatorRay1';
obj.hobby[0] = '运动';

console.log('newObj', newObj);
console.log('obj', obj);

JS手写实现深拷贝(考虑循环引用)_前端_03

两个对象相互影响。

那么,如果我们在这个版本进行循环引用呢?

let obj = {
name: "微信公众号: Code程序人生",
age: 22,
sex: "男",
hobby: ["跑步", "读书", "睡觉"],
fn: function () {
console.log(this.name);
},
};

let obj1 = {};

let newObj = {};

obj.obj1 = obj1;
obj1.obj = obj;

deepClone(obj, newObj);

obj.name = 'CreatorRay1';
obj.hobby[0] = '运动';

console.log('newObj', newObj);
console.log('obj', obj);

JS手写实现深拷贝(考虑循环引用)_前端_04

会直接报错。

考虑循环引用实现深拷贝

在实现兼容循环引用之前,需要铺垫一下​​WeakMap​​​的知识,实现的主要原理就是基于​​WeakMap​​的。

​WeakMap​​​可以算是​​Map​​的一个加强版。

同样都是​​key , value​​​的形式保存变量,​​WeakMap​​​和​​Map​​​最大的区别就是,​​WeakMap​​是弱引用的,当它的键在外部失去引用时,这组键值对自动删除。

基于​​WeakMap​​的原理,我们就可以全面完善深拷贝的代码。

function deepClones(origin, map = new WeakMap()) {
// origin == undefined可以同时判断undefined和null
if (origin == undefined || typeof origin !== "object") {
return origin;
} else if (origin instanceof Date) {
return new Date(origin);
} else if (origin instanceof RegExp) {
return new RegExp(origin);
} else {
const key = map.get(origin);

if (key) {
return key;
}

const target = new origin.constructor();
map.set(origin, target);
for (let k in origin) {
target[k] = deepClones(origin[k], map);
}
return target;
}
}

针对​​Date​​​、​​RegExp​​等类型的变量,也可以额外进行判断。

解释一下这个版本和上个版本的区别:

  • ​origin == undefined​​​可以同时判断​​undefined​​​和​​null​​两种情况
  • ​target​​​不是通过​​Object.prototype.toString​​​来判断当前的属性是数组还是对象了,而是​​new​​​当前属性的​​constructor​​​,对于​​constructor​​​不太了解的同学可以自行补习一下。简单解释就是如果当前属性是数组,它的​​constructor​​​就是​​Array​​​就相当于​​new Array​​​了,属性是对象的话,同理​​new Object​​了。
  • 通过​​WeakMap​​来存储每次递归的键值对,如果当前递归的属性已存在就直接返回。

这个版本和上个版本相对,整体都清晰明了了,而且更加体现了自己的技术含量,如果你能在面试时写出这个版本的深拷贝,相信一定会给你加很多分。

下面我们来检验一下效果。

首先是正常拷贝。

let obj = {
name: "微信公众号: Code程序人生",
age: 22,
sex: "男",
hobby: ["跑步", "读书", "睡觉"],
fn: function () {
console.log(this.name);
},
};

let obj1 = {};

// obj.obj1 = obj1;
// obj1.obj = obj;

let newObj = deepClones(obj);

obj.name = 'CreatorRay1';
obj.hobby[0] = '运动';

console.log('newObj', newObj);
console.log('obj', obj);

我们在使用中有些许的变化,上个版本我们传入了第二个参数,代表我们拷贝后的结果。这个版本就不需要额外传第二个参数了。

JS手写实现深拷贝(考虑循环引用)_前端_05

正常,两个对象未受到彼此影响。

试一下循环应用的情况。

let obj = {
name: "微信公众号: Code程序人生",
age: 22,
sex: "男",
hobby: ["跑步", "读书", "睡觉"],
fn: function () {
console.log(this.name);
},
};

let obj1 = {};

obj.obj1 = obj1;
obj1.obj = obj;

let newObj = deepClones(obj);

obj.name = 'CreatorRay1';
obj.hobby[0] = '运动';

console.log('newObj', newObj);
console.log('obj', obj);

JS手写实现深拷贝(考虑循环引用)_vue.js_06

可以看到没有报错,而且循环引用的值也都正常显示。

欢迎大家关注我的公众号,有很多关于前端的内容哦
公众号:Code程序人生
B站账号:LuckyRay123
个人博客:http://rayblog.ltd/