當咱們進行數據拷貝的時候,若是該數據是一個引用類型,而且拷貝的時候僅僅傳遞的是該對象的指針,那麼就屬於淺拷貝。因爲拷貝過程當中只傳遞了指針,並無從新建立一個新的引用類型對象,因此兩者共享同一片內存空間,即經過指針指向同一片內存空間。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的做用就是給傳入的對象設置了一個屬性而已,不存在被誰引用的關係。