對象深淺拷貝與WeakMap
一、淺拷貝
當(dāng)我們進行數(shù)據(jù)拷貝的時候,如果該數(shù)據(jù)是一個引用類型,并且拷貝的時候僅僅傳遞的是該對象的指針,那么就屬于淺拷貝。由于拷貝過程中只傳遞了指針,并沒有重新創(chuàng)建一個新的引用類型對象,所以二者共享同一片內(nèi)存空間,即通過指針指向同一片內(nèi)存空間。
常見的對象淺拷貝方式為:
① 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值也跟著變化了。
常見的數(shù)組淺拷貝方式為:
① 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屬性值也跟著變化。
二、深拷貝
當(dāng)我們進行數(shù)據(jù)拷貝的時候,如果該數(shù)據(jù)是一個引用類型,并且拷貝的時候,傳遞的不是該對象的指針,而是創(chuàng)建一個新的與之相同的引用類型數(shù)據(jù),那么就屬于深拷貝。由于拷貝過程中重新創(chuàng)建了一個新的引用類型數(shù)據(jù),所以二者擁有獨立的內(nèi)存空間,相互修改不會互相影響。
常見的對象和數(shù)組深拷貝方式為:
① 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中的數(shù)組和對象都沒有受到影響,所以屬于深拷貝。
雖然JSON.stringify()和JSON.parse()能實現(xiàn)深拷貝,但是其并不能處理所有數(shù)據(jù)類型,當(dāng)數(shù)據(jù)為函數(shù)的時候,拷貝的結(jié)果為null;當(dāng)數(shù)據(jù)為正則的時候,拷貝結(jié)果為一個空對象{},如:
const?a?=?{
????fn:?()?=>?{},
????reg:?new?RegExp(/123/)
};
const?b?=?JSON.parse(JSON.stringify(a));
console.log(b);?//?{?reg:?{}?}
可以看到,JSON.stringify()和JSON.parse()對正則和函數(shù)深拷貝無效。
三、實現(xiàn)深拷貝
進行深拷貝的時候,我們主要關(guān)注的是對象類型,即在拷貝對象的時候,該對象必須創(chuàng)建的一個新的對象,如果對象的屬性值仍然為對象,則需要進行遞歸拷貝。對象類型主要為,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))?{?//?數(shù)組
????????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/?}
由于需要進行遞歸拷貝,所以對于非對象類型的數(shù)據(jù)直接返回原值即可。對于Date類型的值,則直接傳入當(dāng)前值new一個Date對象即可,對于RegExp對象的值,也是直接傳入當(dāng)前值new一個RegExp對象即可。對于數(shù)組類型,遍歷數(shù)組的每一項并進行遞歸拷貝即可。對于對象,同樣遍歷對象的所有key值,同時對其值進行遞歸拷貝即可。對于對象還需要考慮屬性值為Symbol的類型,因為Symbol類型的key無法直接通過Object.keys()枚舉到。
三、相互引用問題
上面的深拷貝實現(xiàn)看上去很完善,但是還有一種情況未考慮到,那就是對象相互引用的情況,這種情況將會導(dǎo)致遞歸無法結(jié)束。
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對象呢?
我們也是按照上面的方式進行遞歸拷貝:
//?①?創(chuàng)建一個空的對象,表示對a對象的拷貝結(jié)果
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已經(jīng)拷貝完成,再將bClone賦值給aClone的b屬性即可
aClone.b?=?bClone;
console.log(aClone);?//?{?name:?'a',?b:?{?name:?'b',?a:?[Circular]?}}
其中最關(guān)鍵的就是第④步,這里就是結(jié)束遞歸的關(guān)鍵,我們是拿到了a的拷貝結(jié)果進行了賦值,所以我們需要記錄下某個對象的拷貝結(jié)果,如果之前已經(jīng)拷貝過,那么我們直接拿到拷貝結(jié)果賦值即可完成相互引用。
而JS提供了一種WeakMap數(shù)據(jù)結(jié)構(gòu),其只能用對象作為key值進行存儲,我們可以用拷貝前的對象作為key,拷貝后的結(jié)果對象作為value,當(dāng)出現(xiàn)相互引用關(guān)系的時候,我們只需要從WeakMap對象中取出之前已經(jīng)拷貝的結(jié)果對象賦值即可形成相互引用關(guān)系。
function?deepClone(source,?map?=?new?WeakMap())?{?//?傳入一個WeakMap對象用于記錄拷貝前和拷貝后的映射關(guān)系
????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中取出之前拷貝的結(jié)果對象并返回以便形成相互引用關(guān)系
????????return?map.get(source);
????}
????let?result;
????if?(Array.isArray(source))?{?//?數(shù)組
????????result?=?[];
????????map.set(source,?result);?//?數(shù)組也會存在相互引用
????????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;
????}
}
至此已經(jīng)實現(xiàn)了一個相對比較完善的深拷貝。
四、WeakMap(補充)
WeakMap有一個特點就是屬性值只能是對象,而Map的屬性值則無限制,可以是任何類型。從其名字可以看出,WeakMap是一種弱引用,所以不會造成內(nèi)存泄漏。接下來我們就是要弄清楚為什么是弱引用。
我們首先看看WeakMap的polyfill實現(xiàn),如下:
var?WeakMap?=?function()?{
????this.name?=?'__wm__'?+?uuid();
};
WeakMap.prototype?=?{
????set:?function(key,?value)?{?//?這里的key是一個對象,并且是局部變量
????????Object.defineProperty(key,?this.name,?{?//?給傳入的對象上添加一個this.name屬性,值為要保存的結(jié)果
????????????value:?[key,?value],
????????});
????????return?this;
????},
????get:?function(key)?{
????????var?entry?=?key[this.name];
????????return?entry?&&?(entry[0]?===?key???entry[1]?:?undefined);
????}
};
從WeakMap的實現(xiàn)上我們可以看到,WeakMap并沒有直接引用傳入的對象,當(dāng)我們調(diào)用WeakMap對象set()方法的時候,會傳入一個對象,然后在傳入的對象上添加一個this.name屬性,值為一個數(shù)組,第一項為傳入的對象,第二項為設(shè)置的值,當(dāng)set方法調(diào)用結(jié)束后,局部變量key被釋放,所以WeakMap并沒有直接引用傳入的對象,即弱引用。
其執(zhí)行過程等價于下面的方法調(diào)用:
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的作用就是給傳入的對象設(shè)置了一個屬性而已,不存在被誰引用的關(guān)系。

