1. 背景知识
1.1 JavaScript中的数据类型
-
基础类型分类
undefinednullstringbooleannumbersymbol(es6引入)
-
引用类型分类
- Object
- Array
- Function
1.2 堆 & 栈之分
- 基础数据:存放在栈内存中,因其数据大小确定,内存空间大小可分配,按值存放;
- 引用数据:存放在堆内存中,因其数据大小未知,内存空间大小不确定,按指针地址存放栈内存中,该指向堆内存中的地址存放的实际数据。
2. cloneShallow 浅拷贝 对比 cloneDeep 深拷贝
2.1 cloneShallow 浅拷贝
简单类型的数据,实际上不存在深浅拷贝之分,它们都存放在栈内存中,可直接使用,且是不可变数据,再进行拷贝时,是把值赋给新的变量,新变量值的改变并不会因影响旧变量值
let name = "Tom";
let name2 = name;
name2 = "Jerry"
console.log(name, name === name2);
// Tom false
引用类型的数据,则无法通过=进行赋值拷贝,若按=则得到的只是该变量存放在栈内存中的引用指针,指向在堆内存中的数据一样
let obj = { name: "Tom" };
let obj1 = obj;
obj1.name = "Jerry";
console.log( obj, obj === obj1 );
// { name: "Jerry", true}
LOOK,此时对新对象
obj1的name进行修改,原对象obj的name也发生了改变,且两者在进行===运算时,返回true,可见两者实行的=赋值预算时,只是把引用指针的地址简单赋值,实现的只是浅拷贝。
下面来看下如果实现深度拷贝
2.1 cloneDeep 深拷贝
(1)JSON API
let tom = { name: "tom", [Symbol()]: "symbol", say() {}, a: null, b: undefined };
let tom2 = JSON.parse(JSON.stringify(tom));
console.log(tom2);
// { name: "tom", a: null }
LOOK,上面的方法并不安全,如下罗列
该方法弊端:
- 属性名为
Symbol值,会默认被忽略 - 属性值为
null,function时也会默认忽略掉
综上,该方法不建议使用
(2)手写cloneDeep函数
a. 首先代码展示:
function cloneDeep (obj, hash = new WeakMap()) {
// 类型校验
// if (typeof obj == "undefined" ) return obj;
// 校验null/ undefined不能用上述方法
if (obj == null ) return obj;
if (typeof obj !== "object") return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// []/ {} cloneObj
let val = hash.get(obj);
if (val) { // 映射表 存在 直接将结果返回 避免重复地址
return val; // 递归的终止条件
}
// 获取传入对象/方法的构造函数
let cloneObj = new obj.constructor;
for (let key in obj) {
// 添加对象/ 数组 的自由属性,继承属性过滤
if (obj.hasOwnProperty(key)) {
cloneObj[key] = cloneDeep(obj[key], hash);
hash.set(obj, cloneObj);
}
}
return cloneObj;
}
let null1 = null;
let null2 = cloneDeep(null);
// console.log(null1 === null2); // true
let date = new Date();
let date2 = cloneDeep(date);
console.log(date === date2); // false
let obj = { a: 1};
let obj2 = cloneDeep(obj);
obj2.a = 2;
console.log(obj.a); // 1
let arr = [1, 2, 3, 4];
let arr1 = cloneDeep(arr);
arr1.unshift(0);
console.log(`arr: ${arr}; arr1 ${arr1}`);
// arr: 1,2,3,4; arr1 0,1,2,3,4
let cobj = { a: 1 };
cobj.b = cobj;
let cobj1 = cloneDeep(cobj);
cobj1.a = 2;
console.log(cobj, cobj1);
考察点:类型校验 + hash表
b. 代码解析步骤
先要清楚,哪些属性是不需要进行校验直接返回的:
- 简单类型的值
- 函数
首先,类型校验:
-
typeof
- null 和 undefined
- function
-
instanceof
- Date
- RegExp
-
constructor
- Object
- Array
- Map
- WeakMap
- Set
- WeakSet
- ...
对象属性如果存在循环引用:
let obj = { a: 1, b: 2 };
obj.c = obj;
通过hash表进行解决,同时在循环递归时,运用WeakMap内部属性不可重复,类似(WeakSet特性),可作为判断递归终止条件,同时避免造成内存泄露