作为一个前端,一定对深拷贝这个名词不陌生,深拷贝是非常重要的。
为什么存在深拷贝?
深拷贝这个名词的诞生也是有由来的,在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;
这样就会造成对象间的循环引用,这个也需要考虑在深拷贝的实现之中。
手写实现深拷贝(不考虑循环引用)
我们先实现一个最基本的深拷贝版本,不考虑循环应用。
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);
通过结果可以看到,新创建的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);
两个对象相互影响。
那么,如果我们在这个版本进行循环引用呢?
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);
会直接报错。
考虑循环引用实现深拷贝
在实现兼容循环引用之前,需要铺垫一下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);
我们在使用中有些许的变化,上个版本我们传入了第二个参数,代表我们拷贝后的结果。这个版本就不需要额外传第二个参数了。
正常,两个对象未受到彼此影响。
试一下循环应用的情况。
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);
可以看到没有报错,而且循环引用的值也都正常显示。
欢迎大家关注我的公众号,有很多关于前端的内容哦
公众号:Code程序人生
B站账号:LuckyRay123
个人博客:http://rayblog.ltd/