溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JavaScript中的深拷貝如何實現(xiàn)

發(fā)布時間:2022-10-24 17:54:16 來源:億速云 閱讀:140 作者:iii 欄目:web開發(fā)

今天小編給大家分享一下JavaScript中的深拷貝如何實現(xiàn)的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

深拷貝的最終實現(xiàn)

這里先直接給出最終的代碼版本,方便想快速了解的人查看,當然,你想一步步了解可以繼續(xù)查看文章余下的內容:

function deepClone(target) {
    const map = new WeakMap()
    
    function isObject(target) {
        return (typeof target === 'object' && target ) || typeof target === 'function'
    }

    function clone(data) {
        if (!isObject(data)) {
            return data
        }
        if ([Date, RegExp].includes(data.constructor)) {
            return new data.constructor(data)
        }
        if (typeof data === 'function') {
            return new Function('return ' + data.toString())()
        }
        const exist = map.get(data)
        if (exist) {
            return exist
        }
        if (data instanceof Map) {
            const result = new Map()
            map.set(data, result)
            data.forEach((val, key) => {
                if (isObject(val)) {
                    result.set(key, clone(val))
                } else {
                    result.set(key, val)
                }
            })
            return result
        }
        if (data instanceof Set) {
            const result = new Set()
            map.set(data, result)
            data.forEach(val => {
                if (isObject(val)) {
                    result.add(clone(val))
                } else {
                    result.add(val)
                }
            })
            return result
        }
        const keys = Reflect.ownKeys(data)
        const allDesc = Object.getOwnPropertyDescriptors(data)
        const result = Object.create(Object.getPrototypeOf(data), allDesc)
        map.set(data, result)
        keys.forEach(key => {
            const val = data[key]
            if (isObject(val)) {
                result[key] = clone(val)
            } else {
                result[key] = val
            }
        })
        return result
    }

    return clone(target)
}

1. JavaScript數(shù)據(jù)類型的拷貝原理

先看看JS數(shù)據(jù)類型圖(除了Object,其他都是基礎類型):
JavaScript中的深拷貝如何實現(xiàn)
在JavaScript中,基礎類型值的復制是直接拷貝一份新的一模一樣的數(shù)據(jù),這兩份數(shù)據(jù)相互獨立,互不影響。而引用類型值(Object類型)的復制是傳遞對象的引用(也就是對象所在的內存地址,即指向對象的指針),相當于多個變量指向同一個對象,那么只要其中的一個變量對這個對象進行修改,其他的變量所指向的對象也會跟著修改(因為它們指向的是同一個對象)。如下圖:
JavaScript中的深拷貝如何實現(xiàn)

2. 深淺拷貝

深淺拷貝主要針對的是Object類型,基礎類型的值本身即是復制一模一樣的一份,不區(qū)分深淺拷貝。這里我們先給出測試的拷貝對象,大家可以拿這個obj對象來測試一下自己寫的深拷貝函數(shù)是否完善:

// 測試的obj對象
const obj = {
    // =========== 1.基礎數(shù)據(jù)類型 ===========
    num: 0, // number
    str: '', // string
    bool: true, // boolean
    unf: undefined, // undefined
    nul: null, // null
    sym: Symbol('sym'), // symbol
    bign: BigInt(1n), // bigint

    // =========== 2.Object類型 ===========
    // 普通對象
    obj: {
        name: '我是一個對象',
        id: 1
    },
    // 數(shù)組
    arr: [0, 1, 2],
    // 函數(shù)
    func: function () {
        console.log('我是一個函數(shù)')
    },
    // 日期
    date: new Date(0),
    // 正則
    reg: new RegExp('/我是一個正則/ig'),
    // Map
    map: new Map().set('mapKey', 1),
    // Set
    set: new Set().add('set'),
    // =========== 3.其他 ===========
    [Symbol('1')]: 1  // Symbol作為key
};

// 4.添加不可枚舉屬性
Object.defineProperty(obj, 'innumerable', {
    enumerable: false,
    value: '不可枚舉屬性'
});

// 5.設置原型對象
Object.setPrototypeOf(obj, {
    proto: 'proto'
})

// 6.設置loop成循環(huán)引用的屬性
obj.loop = obj

obj對象在Chrome瀏覽器中的結果:

JavaScript中的深拷貝如何實現(xiàn)

2.1 淺拷貝

淺拷貝: 創(chuàng)建一個新的對象,來接受你要重新復制或引用的對象值。如果對象屬性是基本的數(shù)據(jù)類型,復制的就是基本類型的值給新對象;但如果屬性是引用數(shù)據(jù)類型,復制的就是內存中的地址,如果其中一個對象改變了這個內存中的地址所指向的對象,肯定會影響到另一個對象。

首先我們看看一些淺拷貝的方法(詳細了解可點擊對應方法的超鏈接):

方法使用方式注意事項
Object.assign()Object.assign(target, ...sources)
說明:用于將所有可枚舉屬性的值從一個或多個源對象分配到目標對象。它將返回目標對象。
1.不會拷貝對象的繼承屬性;
2.不會拷貝對象的不可枚舉的屬性;
3.可以拷貝 Symbol 類型的屬性。
展開語法let objClone = { ...obj };缺陷和Object.assign()差不多,但是如果屬性都是基本類型的值,使用擴展運算符進行淺拷貝會更加方便。
Array.prototype.concat()拷貝數(shù)組const new_array = old_array.concat(value1[, value2[, ...[, valueN]]])淺拷貝,適用于基本類型值的數(shù)組
Array.prototype.slice()拷貝數(shù)組arr.slice([begin[, end]])淺拷貝,適用于基本類型值的數(shù)組

這里只列舉了常用的幾種方式,除此之外當然還有其他更多的方式。注意,我們直接使用=賦值不是淺拷貝,因為它是直接指向同一個對象了,并沒有返回一個新對象。

手動實現(xiàn)一個淺拷貝:

function shallowClone(target) {
    if (typeof target === 'object' && target !== null) {
        const cloneTarget = Array.isArray(target) ? [] : {};
        for (let prop in target) {
            if (target.hasOwnProperty(prop)) {
                cloneTarget[prop] = target[prop];
            }
        }
        return cloneTarget;
    } else {
        return target;
    }
}


// 測試
const shallowCloneObj = shallowClone(obj)

shallowCloneObj === obj  // false,返回的是一個新對象
shallowCloneObj.arr === obj.arr  // true,對于對象類型只拷貝了引用

從上面這段代碼可以看出,利用類型判斷(查看typeof),針對引用類型的對象進行 for 循環(huán)遍歷對象屬性賦值給目標對象的屬性(for...in語句以任意順序遍歷一個對象的除Symbol以外的可枚舉屬性,包含原型上的屬性。查看for…in),基本就可以手工實現(xiàn)一個淺拷貝的代碼了。

2.2 深拷貝

深拷貝:創(chuàng)建一個新的對象,將一個對象從內存中完整地拷貝出來一份給該新對象,并從堆內存中開辟一個全新的空間存放新對象,且新對象的修改并不會改變原對象,二者實現(xiàn)真正的分離。

看看現(xiàn)存的一些深拷貝的方法:

方法1:JSON.stringify()

JSON.stringfy() 其實就是將一個 JavaScript 對象或值轉換為 JSON 字符串,最后再用 JSON.parse() 的方法將JSON 字符串生成一個新的對象。(點這了解:JSON.stringfy()、JSON.parse())

使用如下:

function deepClone(target) {
    if (typeof target === 'object' && target !== null) {
        return JSON.parse(JSON.stringify(target));
    } else {
        return target;
    }
}

// 開頭的測試obj存在BigInt類型、循環(huán)引用,JSON.stringfy()執(zhí)行會報錯,所以除去這兩個條件進行測試
const clonedObj = deepClone(obj)

// 測試
clonedObj === obj  // false,返回的是一個新對象
clonedObj.arr === obj.arr  // false,說明拷貝的不是引用

瀏覽器執(zhí)行結果:

JavaScript中的深拷貝如何實現(xiàn)
從以上結果我們可知JSON.stringfy() 存在以下一些問題:

  • 執(zhí)行會報錯:存在BigInt類型、循環(huán)引用。

  • 拷貝Date引用類型會變成字符串。

  • 鍵值會消失:對象的值中為Function、UndefinedSymbol 這幾種類型,。

  • 鍵值變成空對象:對象的值中為MapSet、RegExp這幾種類型。

  • 無法拷貝:不可枚舉屬性、對象的原型鏈。

  • 補充:其他更詳細的內容請查看官方文檔:JSON.stringify()

由于以上種種限制條件,JSON.stringfy() 方式僅限于深拷貝一些普通的對象,對于更復雜的數(shù)據(jù)類型,我們需要另尋他路。

方法2:遞歸基礎版深拷貝

手動遞歸實現(xiàn)深拷貝,我們只需要完成以下2點即可:

  • 對于基礎類型,我們只需要簡單地賦值即可(使用=)。

  • 對于引用類型,我們需要創(chuàng)建新的對象,并通過遍歷鍵來賦值對應的值,這個過程中如果遇到 Object 類型還需要再次進行遍歷。

function deepClone(target) {
    if (typeof target === 'object' && target) {
        let cloneObj = {}
        for (const key in target) { // 遍歷
            const val = target[key]
            if (typeof val === 'object' && val) {
                cloneObj[key] = deepClone(val) // 是對象就再次調用該函數(shù)遞歸
            } else {
                cloneObj[key] = val // 基本類型的話直接復制值
            }
        }
        return cloneObj
    } else {
        return target;
    }
}

// 開頭的測試obj存在循環(huán)引用,除去這個條件進行測試
const clonedObj = deepClone(obj)

// 測試
clonedObj === obj  // false,返回的是一個新對象
clonedObj.arr === obj.arr  // false,說明拷貝的不是引用

瀏覽器執(zhí)行結果:

JavaScript中的深拷貝如何實現(xiàn)
該基礎版本存在許多問題:

  • 不能處理循環(huán)引用。

  • 只考慮了Object對象,而Array對象、Date對象、RegExp對象、Map對象、Set對象都變成了Object對象,且值也不正確。

  • 丟失了屬性名為Symbol類型的屬性。

  • 丟失了不可枚舉的屬性。

  • 原型上的屬性也被添加到拷貝的對象中了。

如果存在循環(huán)引用的話,以上代碼會導致無限遞歸,從而使得堆棧溢出。如下例子:

const a = {}
const b = {}
a.b = b
b.a = a
deepClone(a)

對象 a 的鍵 b 指向對象 b,對象 b 的鍵 a 指向對象 a,查看a對象,可以看到是無限循環(huán)的:
JavaScript中的深拷貝如何實現(xiàn)
對對象a執(zhí)行深拷貝,會出現(xiàn)死循環(huán),從而耗盡內存,進而報錯:堆棧溢出
JavaScript中的深拷貝如何實現(xiàn)
如何避免這種情況呢?一種簡單的方式就是把已添加的對象記錄下來,這樣下次碰到相同的對象引用時,直接指向記錄中的對象即可。要實現(xiàn)這個記錄功能,我們可以借助 ES6 推出的 WeakMap 對象,該對象是一組鍵/值對的集合,其中的鍵是弱引用的。其鍵必須是對象,而值可以是任意的。(WeakMap相關見這:WeakMap)

針對以上基礎版深拷貝存在的缺陷,我們進一步去完善,實現(xiàn)一個完美的深拷貝。

方法3:遞歸完美版深拷貝

對于基礎版深拷貝存在的問題,我們一一改進:

存在的問題改進方案
1. 不能處理循環(huán)引用使用 WeakMap 作為一個Hash表來進行查詢
2. 只考慮了Object對象當參數(shù)為 Date、RegExp 、FunctionMap、Set,則直接生成一個新的實例返回
3. 屬性名為Symbol的屬性
4. 丟失了不可枚舉的屬性
針對能夠遍歷對象的不可枚舉屬性以及 Symbol 類型,我們可以使用 Reflect.ownKeys()
Reflect.ownKeys(obj)相當于[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]
4. 原型上的屬性Object.getOwnPropertyDescriptors()設置屬性描述對象,以及Object.create()方式繼承原型鏈

代碼實現(xiàn):

function deepClone(target) {
    // WeakMap作為記錄對象Hash表(用于防止循環(huán)引用)
    const map = new WeakMap()

    // 判斷是否為object類型的輔助函數(shù),減少重復代碼
    function isObject(target) {
        return (typeof target === 'object' && target ) || typeof target === 'function'
    }

    function clone(data) {

        // 基礎類型直接返回值
        if (!isObject(data)) {
            return data
        }

        // 日期或者正則對象則直接構造一個新的對象返回
        if ([Date, RegExp].includes(data.constructor)) {
            return new data.constructor(data)
        }

        // 處理函數(shù)對象
        if (typeof data === 'function') {
            return new Function('return ' + data.toString())()
        }

        // 如果該對象已存在,則直接返回該對象
        const exist = map.get(data)
        if (exist) {
            return exist
        }

        // 處理Map對象
        if (data instanceof Map) {
            const result = new Map()
            map.set(data, result)
            data.forEach((val, key) => {
                // 注意:map中的值為object的話也得深拷貝
                if (isObject(val)) {
                    result.set(key, clone(val))
                } else {
                    result.set(key, val)
                }
            })
            return result
        }

        // 處理Set對象
        if (data instanceof Set) {
            const result = new Set()
            map.set(data, result)
            data.forEach(val => {
                // 注意:set中的值為object的話也得深拷貝
                if (isObject(val)) {
                    result.add(clone(val))
                } else {
                    result.add(val)
                }
            })
            return result
        }

        // 收集鍵名(考慮了以Symbol作為key以及不可枚舉的屬性)
        const keys = Reflect.ownKeys(data)
        // 利用 Object 的 getOwnPropertyDescriptors 方法可以獲得對象的所有屬性以及對應的屬性描述
        const allDesc = Object.getOwnPropertyDescriptors(data)
        // 結合 Object 的 create 方法創(chuàng)建一個新對象,并繼承傳入原對象的原型鏈, 這里得到的result是對data的淺拷貝
        const result = Object.create(Object.getPrototypeOf(data), allDesc)

        // 新對象加入到map中,進行記錄
        map.set(data, result)

        // Object.create()是淺拷貝,所以要判斷并遞歸執(zhí)行深拷貝
        keys.forEach(key => {
            const val = data[key]
            if (isObject(val)) {
                // 屬性值為 對象類型 或 函數(shù)對象 的話也需要進行深拷貝
                result[key] = clone(val)
            } else {
                result[key] = val
            }
        })
        return result
    }

    return clone(target)
}



// 測試
const clonedObj = deepClone(obj)
clonedObj === obj  // false,返回的是一個新對象
clonedObj.arr === obj.arr  // false,說明拷貝的不是引用
clonedObj.func === obj.func  // false,說明function也復制了一份
clonedObj.proto  // proto,可以取到原型的屬性

在遍歷 Object 類型數(shù)據(jù)時,我們需要把 Symbol 類型的鍵名也考慮進來,所以不能通過 Object.keys 獲取鍵名或 for...in 方式遍歷,而是通過Reflect.ownKeys()獲取所有自身的鍵名(getOwnPropertyNamesgetOwnPropertySymbols 函數(shù)將鍵名組合成數(shù)組也行:[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]),然后再遍歷遞歸,最終實現(xiàn)拷貝。

瀏覽器執(zhí)行結果:
JavaScript中的深拷貝如何實現(xiàn)
可以發(fā)現(xiàn)我們的cloneObj對象和原來的obj對象一模一樣,并且修改cloneObj對象的各個屬性都不會對obj對象造成影響。其他的大家再多嘗試體會哦!

以上就是“JavaScript中的深拷貝如何實現(xiàn)”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI