当咱们进行数据拷贝的时候,若是该数据是一个引用类型,而且拷贝的时候仅仅传递的是该对象的指针,那么就属于浅拷贝。因为拷贝过程当中只传递了指针,并无从新建立一个新的引用类型对象,因此两者共享同一片内存空间,即经过指针指向同一片内存空间。javascript
常见的对象浅拷贝方式为:
① Object.assign()java
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()ui
const a = [{name: "lihb"}]; const b = a.slice(); a[0].name = "lily"; console.log(b[0].name); // lily
一旦修改对象a[0]的name属性值,克隆的对象b[0]的name属性值也跟着变化,因此属于浅拷贝。this
② concat()prototype
const a = [{name: "lihb"}]; const b = a.concat(); a[0].name = "lily"; console.log(b[0].name);// lily
一样的,修改对象a[0]的name属性值,克隆的对象b[0]的name属性值也跟着变化。指针
③ 扩展运算符(...)code
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有一个特色就是属性值只能是对象,而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的做用就是给传入的对象设置了一个属性而已,不存在被谁引用的关系。