共计 5214 个字符,预计需要花费 14 分钟才能阅读完成。
一、浅拷贝
当咱们进行数据拷贝的时候,如果该数据是一个 援用类型 ,并且拷贝的时候 仅仅传递的是该对象的指针 ,那么就属于浅拷贝。因为拷贝过程中只传递了指针, 并没有从新创立一个新的援用类型对象 ,所以 二者共享同一片内存空间,即通过指针指向同一片内存空间。
常见的 对象 浅拷贝形式为:
① Object.assign()
const a = {msg: {name: "lihb"}};
const b = Object.assign({}, a);
a.msg.name = "lily";
console.log(b.msg.name); // lily
一旦批改对象 a 的 msg 的 name 属性值,克隆的 b 对象的 msg 的 name 属性也跟着变动了,所以属于浅拷贝。
② 扩大运算符(…)
const a = {msg: {name: "lihb"}};
const b = {...a};
a.msg.name = "lily";
console.log(b.msg.name); // lily
同样的,批改对象 a 中的 name,克隆对象 b 中的 name 值也跟着变动了。
常见的 数组 浅拷贝形式为:
① slice()
const a = [{name: "lihb"}];
const b = a.slice();
a[0].name = "lily";
console.log(b[0].name); // lily
一旦批改对象 a[0]的 name 属性值,克隆的对象 b[0]的 name 属性值也跟着变动,所以属于浅拷贝。
② concat()
const a = [{name: "lihb"}];
const b = a.concat();
a[0].name = "lily";
console.log(b[0].name);// lily
同样的,批改对象 a[0]的 name 属性值,克隆的对象 b[0]的 name 属性值也跟着变动。
③ 扩大运算符(…)
const a = [{name: "lihb"}];
const b = [...a];
a[0].name = "lily";
console.log(b[0].name); // lily
同样的,批改对象 a[0]的 name 属性值,克隆的对象 b[0]的 name 属性值也跟着变动。
二、深拷贝
当咱们进行数据拷贝的时候,如果该数据是一个 援用类型 ,并且拷贝的时候,传递的不是该对象的指针,而是 创立一个新的与之雷同的援用类型数据 ,那么就属于深拷贝。因为拷贝过程中从新创立了一个新的援用类型数据,所以 二者领有独立的内存空间,互相批改不会相互影响。
常见的 对象和数组 深拷贝形式为:
① JSON.stringify()和 JSON.parse()
const a = {msg: {name: "lihb"}, arr: [1, 2, 3]};
const b = JSON.parse(JSON.stringify(a));
a.msg.name = "lily";
console.log(b.msg.name); // lihb
a.arr.push(4);
console.log(b.arr[4]); // undefined
能够看到,对对象 a 进行批改后,拷贝的对象 b 中的数组和对象都没有受到影响,所以属于深拷贝。
尽管 JSON.stringify()和 JSON.parse()能实现深拷贝,然而其并不能解决所有数据类型 ,当数据为 函数 的时候,拷贝的后果为 null;当数据为 正则 的时候,拷贝后果为一个 空对象{},如:
const a = {fn: () => {},
reg: new RegExp(/123/)
};
const b = JSON.parse(JSON.stringify(a));
console.log(b); // {reg: {} }
能够看到,JSON.stringify()和 JSON.parse()对正则和函数深拷贝有效。
三、实现深拷贝
进行深拷贝的时候,咱们次要 关注的是对象类型 ,即在拷贝对象的时候, 该对象必须创立的一个新的对象 ,如果对象的属性值依然为对象,则须要进行 递归拷贝 。对象类型次要为,Date、RegExp、Array、Object 等。
function deepClone(source) {if (typeof source !== "object") {// 非对象类型(undefined、boolean、number、string、symbol),间接返回原值即可
return source;
}
if (source === null) { // 为 null 类型的时候
return source;
}
if (source instanceof Date) { // Date 类型
return new Date(source);
}
if (source instanceof RegExp) { // RegExp 正则类型
return new RegExp(source);
}
let result;
if (Array.isArray(source)) { // 数组
result = [];
source.forEach((item) => {result.push(deepClone(item));
});
return result;
} else { // 为对象的时候
result = {};
const keys = [...Object.getOwnPropertyNames(source), ...Object.getOwnPropertySymbols(source)]; // 取出对象的 key 以及 symbol 类型的 key
keys.forEach(key => {let item = source[key];
result[key] = deepClone(item);
});
return result;
}
}
let a = {name: "a", msg: {name: "lihb"}, date: new Date("2020-09-17"), reg: new RegExp(/123/)};
let b = deepClone(a);
a.msg.name = "lily";
a.date = new Date("2020-08-08");
a.reg = new RegExp(/456/);
console.log(b);
// {name: 'a', msg: { name: 'lihb'}, date: 2020-09-17T00:00:00.000Z, reg: /123/ }
因为须要进行递归拷贝,所以 对于非对象类型的数据间接返回原值即可 。对于 Date 类型的值,则间接传入以后值 new 一个 Date 对象即可,对于 RegExp 对象的值,也是间接传入以后值 new 一个 RegExp 对象即可。对于数组类型,遍历数组的每一项并进行递归拷贝即可。对于对象,同样遍历对象的所有 key 值,同时对其值进行递归拷贝即可。 对于对象还须要思考属性值为 Symbol 的类型 ,因为Symbol 类型的 key 无奈间接通过 Object.keys() 枚举到。
三、互相援用问题
下面的深拷贝实现看上去很欠缺,然而还有一种状况未思考到,那就是 对象互相援用 的状况,这种状况将会导致 递归无奈完结。
const a = {name: "a"};
const b = {name: "b"};
a.b = b;
b.a = a; // 互相援用
console.log(a); // {name: 'a', b: { name: 'b', a: [Circular] } }
对于下面这种状况,咱们须要怎么拷贝 互相援用后的 a 对象 呢?
咱们也是依照下面的形式进行递归拷贝:
// ① 创立一个空的对象,示意对 a 对象的拷贝后果
const aClone = {};
// ② 遍历 a 中的属性,name 和 b, 首先拷贝 name 属性和 b 属性
aClone.name = a.name;
// ③ 接着拷贝 b 属性,而 b 的属性值为 b 对象,须要进行递归拷贝,同时蕴含 name 和 a 属性,先拷贝 name 属性
const bClone = {};
bClone.name = b.name;
// ④ 接着拷贝 a 属性,而 a 的属性值为 a 对象,咱们须要将之前 a 的拷贝对象 aClone 赋值即可
bClone.a = aClone;
// ⑤ 此时 bClone 曾经拷贝实现,再将 bClone 赋值给 aClone 的 b 属性即可
aClone.b = bClone;
console.log(aClone); // {name: 'a', b: { name: 'b', a: [Circular] } }
其中最要害的就是第④步,这里就是完结递归的要害,咱们是拿到了 a 的拷贝后果进行了赋值,所以咱们 须要记录下某个对象的拷贝后果 ,如果之前曾经拷贝过,那么咱们 间接拿到拷贝后果赋值即可实现互相援用 。
而 JS 提供了一种 WeakMap 数据结构,其只能用对象作为 key 值进行存储 ,咱们能够 用拷贝前的对象作为 key,拷贝后的后果对象作为 value,当呈现互相援用关系的时候,咱们只须要从 WeakMap 对象中取出之前曾经拷贝的后果对象赋值即可造成互相援用关系。
function deepClone(source, map = new WeakMap()) { // 传入一个 WeakMap 对象用于记录拷贝前和拷贝后的映射关系
if (typeof source !== "object") {// 非对象类型(undefined、boolean、number、string、symbol),间接返回原值即可
return source;
}
if (source === null) { // 为 null 类型的时候
return source;
}
if (source instanceof Date) { // Date 类型
return new Date(source);
}
if (source instanceof RegExp) { // RegExp 正则类型
return new RegExp(source);
}
if (map.get(source)) { // 如果存在互相援用,则从 map 中取出之前拷贝的后果对象并返回以便造成互相援用关系
return map.get(source);
}
let result;
if (Array.isArray(source)) { // 数组
result = [];
map.set(source, result); // 数组也会存在互相援用
source.forEach((item) => {result.push(deepClone(item, map));
});
return result;
} else { // 为对象的时候
result = {};
map.set(source, result); // 保留已拷贝的对象
const keys = [...Object.getOwnPropertyNames(source), ...Object.getOwnPropertySymbols(source)]; // 取出对象的 key 以及 symbol 类型的 key
keys.forEach(key => {let item = source[key];
result[key] = deepClone(item, map);
});
return result;
}
}
至此曾经实现了一个 绝对比较完善的 深拷贝。
四、WeakMap(补充)
WeakMap 有一个特点就是 属性值只能是对象 ,而 Map 的属性值则 无限度,能够是任何类型。从其名字能够看出,WeakMap 是一种弱援用,所以不会造成内存透露。接下来咱们就是要弄清楚为什么其是弱援用。
咱们首先看看 WeakMap 的 polyfill 实现,如下:
var WeakMap = function() {this.name = '__wm__' + uuid();
};
WeakMap.prototype = {set: function(key, value) { // 这里的 key 是一个对象,并且是局部变量
Object.defineProperty(key, this.name, { // 给传入的对象上增加一个 this.name 属性,值为要保留的后果
value: [key, value],
});
return this;
},
get: function(key) {var entry = key[this.name];
return entry && (entry[0] === key ? entry[1] : undefined);
}
};
从 WeakMap 的实现上咱们能够看到,WeakMap 并没有间接援用传入的对象 ,当咱们调用 WeakMap 对象 set() 办法的时候,会传入一个对象,而后在传入的对象上增加一个 this.name 属性,值为一个数组,第一项为传入的对象,第二项为设置的值,当 set 办法调用完结后 , 局部变量 key 被开释,所以 WeakMap 并没有间接援用传入的对象,即弱援用。
其执行过程等价于上面的办法调用:
var obj = {name: "lihb"};
function set(key, value) {
var k = "this.name"; // 这里模仿 this.name 的值作为 key
key[k] = [key, value];
}
set(obj, "test"); // 这里模仿 WeakMap 的 set()办法
obj = null; // obj 将会被垃圾回收器回收
所以set 的作用就是给传入的对象设置了一个属性而已,不存在被谁援用的关系。