JS进阶 - 实现深拷贝
实现 JS 变量的深拷贝
铺垫
首先来了解一下 JS 中的变量赋值。
基本数据类型,其变量的值是存放在栈内存中的,赋值操作就是直接拷贝;
var a = 3;
var b = a;
b++;
console.log(a); // return 3;
console.log(b); // return 4;
引用类型,因为值存放于堆内存中,赋值操作其实是拷贝对内存的引用;
var arr = [1,2,3];
var brr = arr;
brr.push(4);
console.log(arr); // return [1,2,3,4]
console.log(brr); // return [1,2,3,4]
不过需要注意的一点就是,如果对参数直接进行赋值操作,并不会修改参数的引用地址,但如果对参数的值进行修改,那么会影响参数的值:
var a = [1,2,3];
function foo(x){
x = [3,2,1];
x.push(4);
};
foo(a);
console.log(a); // [1,2,3]
var a = [1,2,3];
function foo(x){
x.push(4);
};
foo(a);
console.log(a); // [1,2,3,4]
原理
对于基本数据类型来说,没有浅拷贝和深拷贝的区别;
对于引用类型来说,需要将其一步步拆解成基本数据类型进行拷贝;
如此循环,就形成了深拷贝。
基础实现
对传入的对象的属性进行循环,依次加到新创建的对象中
function deepClone(source) {
const target = {};
for (let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
return target;
}
// 测试用例
const test = {
a: 1,
b: {
c: 2,
d: 3
}
}
const instance = deepClone(test);
test.a = 4;
test.b.c = 5;
console.log(instance.a); // 1
console.log(instance.b.c); // 5
缺点:
- 没有对传入的数据类型进行判断,对于基础数据类型可以直接返回。
- 没有对对象进行深度遍历,对象嵌套对象时,还是会出现浅拷贝。
这里要区分三个方法:for...in
、Object.keys()
、Object.getOwnPropertyNames()
for...in
:会循环出除了 Symbol 以外的可枚举属性,属性中可能含有来自原型的属性,所以上述代码中用hasOwnProperty
判断是否是对象本身的属性;
Object.keys()
:循环出自身所有可枚举属性,相当于 for…in 和 hasOwnProperty 判断的结合;
Object.getOwnPropertyNames()
:循环出自身的所有属性(不考虑可枚举属性);
增加判断
function deepClone(source) {
// 如果 source 为 null/undefined 或者 source 不是引用类型,则直接返回
if (!source || typeof source !== "object") {
return source;
}
const target = {};
Object.keys(source).forEach(function(key){
if (typeof source[key] === "object") {
target[key] = deepClone(source[key]);
} else {
target[key] = source[key];
}
})
return target;
}
// 测试用例
const test = {
a: 1,
b: {
c: 2,
d: new RegExp("abc"),
e: [1,2,3]
}
};
const instance = deepClone(test);
test.a = 4;
test.b.c = 5;
test.b.e[0] = 4;
console.log(instance.a); // 1
console.log(instance.b.c); // 2
console.log(instance.b.d); // {}
console.log(instance.b.e); // {0:1,1:2,2:3}
从输出可以看到:
- 因为正则是特殊的对象,所以他会被再次遍历,然而这是不正确的,所以需要对
typeof X === 'object'
的情况进行再次的判断,剔除那些特殊的对象; - 对于数组类型,需要进行特殊处理;
处理特殊对象
function cloneRegExp(source) {
const pattern = source.valueOf();
let flags = "";
flags += pattern.global ? "g" : "";
flags += pattern.ignoreCase ? "i" : "";
flags += pattern.multiline ? "m" : "";
return new RegExp(pattern.source, flags);
}
function deepClone(source) {
if (!source || typeof source !== "object") {
return source;
}
const target = Array.isArray(source) ? [] : {}; // 判断当前是否为数组类型
Object.keys(source).forEach(function(key){
if (typeof source[key] === "object") {
switch (Object.prototype.toString.call(source[key])) {
case "[object Date]":
target[key] = new Date(source[key].valueOf());
break;
case "[object RegExp]":
target[key] = cloneRegExp(source[key]);
break;
default:
target[key] = deepClone(source[key]);
break;
}
} else {
target[key] = source[key];
}
}
return target;
}
// 测试用例
const test = {
a: new Date(),
b: new RegExp("abc")
};
const instance = deepClone(test);
console.log(instance.a); // Sun Mar 01 2020 16:19:10 GMT+0800 (中国标准时间)
console.log(instance.b); // /abc/
上面的代码基本解决了对象类型的问题,不过存在循环调用的问题,举个例子:
let a = {};
a.a = a;
如果直接拷贝的话,会出现递归爆栈的情况。
解决循环调用
要解决爆栈的问题,可以使用哈希表,将出现的每个对象都存储在哈希表中,拷贝的时候先查询需要拷贝的对象是否存在表中,如果存在直接取出值返回即可。
哈希表使用 ES6 的 WeakMap 实现:
function cloneRegExp(source) {
const pattern = source.valueOf();
let flags = "";
flags += pattern.global ? "g" : "";
flags += pattern.ignoreCase ? "i" : "";
flags += pattern.multiline ? "m" : "";
return new RegExp(pattern.source, flags);
}
function deepClone(source, hashMap = new WeakMap()) {
if (!source || typeof source !== "object") {
return source;
}
if (hashMap.has(source)) return hashMap.get(source); // 判断表中是否存在该对象
const target = Array.isArray(source) ? [] : {};
hashMap.set(source, target); // 设置对象
Object.keys(source).forEach(function(key){
if (typeof source[key] === "object") {
switch (Object.prototype.toString.call(source[key])) {
case "[object Date]":
target[key] = new Date(source[key].valueOf());
break;
case "[object RegExp]":
target[key] = cloneRegExp(source[key]);
break;
default:
// 递归时传入当前哈希表
target[key] = deepClone(source[key], hashMap);
break;
}
} else {
target[key] = source[key];
}
}
return target;
}
JS进阶 - 实现深拷贝