對象深淺拷貝與WeakMap

1、淺拷貝

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

2、深拷貝

當咱們進行數據拷貝的時候,若是該數據是一個引用類型,而且拷貝的時候,傳遞的不是該對象的指針,而是建立一個新的與之相同的引用類型數據,那麼就屬於深拷貝。因爲拷貝過程當中從新建立了一個新的引用類型數據,因此兩者擁有獨立的內存空間,相互修改不會互相影響

常見的對象和數組深拷貝方式爲:
① 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()對正則和函數深拷貝無效

3、實現深拷貝

進行深拷貝的時候,咱們主要關注的是對象類型,即在拷貝對象的時候,該對象必須建立的一個新的對象,若是對象的屬性值仍然爲對象,則須要進行遞歸拷貝。對象類型主要爲,DateRegExpArrayObject等。

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()枚舉到

3、相互引用問題

上面的深拷貝實現看上去很完善,可是還有一種狀況未考慮到,那就是對象相互引用的狀況,這種狀況將會致使遞歸沒法結束

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;
    }
}

至此已經實現了一個相對比較完善的深拷貝。

4、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的做用就是給傳入的對象設置了一個屬性而已,不存在被誰引用的關係

相關文章
相關標籤/搜索