【JavaScript】對象的淺拷貝與深拷貝

前言

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

2018-07-31更新: 循環引用以及包裝對象拷貝bash

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 等基礎類型,它是一個引用類型,也就說它的值是保存在堆上,經過內存地址來訪問的。簡單來看post

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

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

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)
複製代碼

能夠看出拷貝的結果仍是使人滿意的。this

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

遍歷拷貝

2.3 Object.assign(target, source)

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

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

2.4 擴展運算符

const simpleClonePerson = {...person}
複製代碼

擴展運算符

可是這裏有個問題,原型對象丟失了。沒法判斷 simpleClonePerson 的實例。3d

可是操做一下 clonePerson.friend 對象,給它添加一個屬性就會發現,person 對應的也增長了一個新屬性。這不是咱們的預期。code

也就說經過 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 吧,哈哈哈哈哈。

相關文章
相關標籤/搜索