【JavaScript】物件的淺拷貝與深拷貝

眼已望穿發表於2018-07-29

前言

在 JavaScript 中,物件可謂是一個非常重要的知識點。什麼原型鏈啊,拷貝啊,繼承啊,建立啊等等等等。在我之前的文章中已經對物件的建立和繼承做了一個簡單的介紹,【JavaScript】ES5/ES6 建立物件與繼承,那麼這篇文章主要是針對物件的拷貝。

2018-07-31更新: 迴圈引用以及包裝物件拷貝

1. 拷貝前的準備

我們先定義一個建構函式,建立好一個等待拷貝的物件。以下操作不考慮迴圈引用、Date 物件以及 RegExp 物件的拷貝等問題。

function Person(name, age, job, ) {
    this.name = name
    this.age = age
    this.job = job
    this.height = function () { }
    this.weight = Symbol.for('weight')
    this.friend = {
        name: 'kangkan',
        age: 15
    }
}

Person.prototype.hobby = function () {
    return ['football', 'basketball']
}

const person = new Person('mike', null, undefined)
複製程式碼

2. 淺拷貝

物件不同於 Number、String 等基礎型別,它是一個引用型別,也就說它的值是儲存在堆上,通過記憶體地址來訪問的。簡單來看

const a = {one: 1}
const b = {one: 1}
a === b // false
複製程式碼

如果 obejct1 的引用地址和 object2 一致,那麼這就是淺拷貝,實現方式有三種。

2.1 直接賦值

const a = {one: 1}
const b = a
b === a // true
a.two = 2
console.log(b.two) // 2
複製程式碼

2.2 遍歷拷貝

const simpleClone = function (target) {
    if (typeof target !== 'object') {
        throw new TypeError('arguments must be a Object!')
    }
    let obj = {}
    // 設定原型
    const prototype = Reflect.getPrototypeOf(target)
    Reflect.setPrototypeOf(obj, prototype)
    // 設定屬性
    Reflect.ownKeys(target).forEach((key) => {
        obj[key] = target[key]
    })
    return obj
}
const clonePerson = simpleClone(person)
複製程式碼

可以看出拷貝的結果還是令人滿意的。

下圖 Object.assign(person) 應為 Object.assign({}, person)

遍歷拷貝

2.3 Object.assign(target, source)

通過這個方法也能達到相同的效果

const simpleClonePerson = Object.assign({}, person)
複製程式碼

2.4 擴充套件運算子

const simpleClonePerson = {...person}
複製程式碼

擴充套件運算子

但是這裡有個問題,原型物件丟失了。無法判斷 simpleClonePerson 的例項。

但是操作一下 clonePerson.friend 物件,給它新增一個屬性就會發現,person 對應的也增加了一個新屬性。這不是我們的預期。

也就說通過 simpleClone 和 Object.assign 拷貝的物件只有第一層是深拷貝,第二層就是淺拷貝了。是對引用地址的拷貝。

3. 深拷貝

簡單來說,以上的淺拷貝方法,在物件深度只有一層的時候其實就是深拷貝。但是當物件的深度大於1,那麼物件裡面的物件就無法完成深拷貝了。

深拷貝的方法也有兩種。

3.1 利用 JSON

const clonePerson = JSON.parse(JSON.stringify(person))
複製程式碼

JSON

從圖中也能看出來,利用 JSON 的方法也是會有很多缺點的。

缺點1:會忽略 undefined

缺點2:不能序列化函式

缺點3:無法拷貝 Symbol

3.2 遞迴拷貝

遞迴拷貝其實也就是在淺拷貝的遍歷拷貝上新增了一些東西

const deepClone = function (target) {
    if (typeof target !== 'object') {
        throw new TypeError('arguments must be a Object!')
    }
    let obj = {}
    // 設定原型
    const prototype = Reflect.getPrototypeOf(target)
    Reflect.setPrototypeOf(obj, prototype)
    // 設定屬性
    Reflect.ownKeys(target).forEach((key) => {
        const value = target[key]
        if (value !== null && typeof value === 'object') {
            obj[key] = deepClone(value)
        } else {
            obj[key] = value
        }
    })
    return obj
}
複製程式碼

遞迴拷貝

達到了想要的效果。

4. 補充

4.1 關於 Date、RegExp 物件的拷貝

我們擴充套件一下 Person 建構函式

function Person(name, age, job, ) {
    this.name = name
    this.age = age
    this.job = job
    this.height = function () { }
    this.weight = Symbol.for('weight')
    this.friend = {
        name: 'kangkan',
        age: 15
    }
    this.family = new Person2()
    this.date = new Date('2018-06-06')
    this.regExp = /test/ig
}

function Person2() { }
複製程式碼

可以看到這裡就多了一個 date 屬性和 regExp 屬性,如果通過之前普通的 deepClone 的話,會出現如下結果。

拷貝包裝物件

所以我們需要對 deepClone 方法進行一定的改造

const deepClone = function (target) {
    if (typeof target !== 'object') {
        throw new TypeError('arguments must be a Object!')
    }
    let obj = {}
    // 設定原型
    const prototype = Reflect.getPrototypeOf(target)
    Reflect.setPrototypeOf(obj, prototype)
    // 設定屬性
    Reflect.ownKeys(target).forEach((key) => {
        const value = target[key]
        // 在此處進行改造
        try {
            const Constructor = Reflect.getPrototypeOf(value).constructor
            // 這裡只針對 Date 物件和 RegExp 物件進行簡單的說明
            if (Constructor === Date || Constructor === RegExp) {
                obj[key] = new Constructor(value.valueOf())
            } else {
                obj[key] = deepClone(value)
            }
        } catch (e) {
            obj[key] = value
        }
    })
    return obj
}
複製程式碼

我們再來看看列印結果

準備拷貝包裝物件

4.2 關於迴圈引用的問題簡述:

person.family = person // 此處出現迴圈引用
const deepClone = function (target) {
    if (typeof target !== 'object') {
        throw new TypeError('arguments must be a Object!')
    }
    let obj = {}
    // 設定原型
    const prototype = Reflect.getPrototypeOf(target)
    Reflect.setPrototypeOf(obj, prototype)
    // 設定屬性
    Reflect.ownKeys(target).forEach((key) => {
        const value = target[key]
        try {
            const Constructor = Reflect.getPrototypeOf(value).constructor
            if (Constructor === Date || Constructor === RegExp) {
                obj[key] = new Constructor(value.valueOf())
            } else {
                obj[key] = deepClone(value)
            }
        } catch (e) {
            obj[key] = value
        }
    })
    return obj
}
複製程式碼

迴圈引用

由上圖可以看到,通過 deepClone 方法進行深拷貝,一旦出現迴圈引用會導致棧溢位。

我們需要對 deepClone 方法再次進行改造

const deepClone = function (target) {
    if (typeof target !== 'object') {
        throw new TypeError('arguments must be a Object!')
    }
    // 已經訪問過的物件集合
    const visitedObjs = []
    // 克隆的物件集合
    const clonedObjs = []
    const clone = function (source) {
        if (visitedObjs.indexOf(source) === -1) { // 這裡是判斷該原物件是否被訪問過
            visitedObjs.push(source) // 放入陣列中
            const obj = {} // 建立一個待克隆的新物件
            // 設定原型
            const prototype = Reflect.getPrototypeOf(source)
            Reflect.setPrototypeOf(obj, prototype)
            clonedObjs.push(obj); // 將其置入克隆物件集合中
            // 設定屬性
            Reflect.ownKeys(source).forEach((key) => {
                const value = source[key]
                try {
                    const Constructor = Reflect.getPrototypeOf(value).constructor
                    if (Constructor === Date || Constructor === RegExp) {
                        obj[key] = new Constructor(value.valueOf())
                    } else {
                        obj[key] = clone(value) // 此處不能再遞迴呼叫 deepClone,而是要改為 clone 方法
                    }
                } catch (e) {
                    obj[key] = value
                }
            })
            return obj
        } else {
            // 如果該物件已經被訪問過了,則直接從克隆物件中返回。返回的物件的索引是 source 在 visitedObjs 中的索引位置。
            return clonedObjs[visitedObjs.indexOf(source)]
        }
    }
    return clone(target)
}
複製程式碼

再來看看效果

迴圈引用拷貝

總結

寫了這麼多主要還是瞭解一些物件的拷貝問題,從上面的一步步改造也可以看出來要真想寫完美這個功能也是得一番功夫的。所以最後大家還是去用 lodash 吧,哈哈哈哈哈。

相關文章