讀書雜記-js對象拷貝

爲何要對js的對象進行拷貝?

var a = { a: 'a' }
var b = a
b.a = 'b'
a.a // 'b'

由於js中的對象(引用數據類型)保存在堆中,而多個變量引用同一塊堆中的內容時,其中一個修改,則會影響全部引用改內容的變量;有時候咱們不但願這樣es6

實現拷貝有什麼辦法?

  • 方法一

淺拷貝,對目標對象進行遍歷,而後將其屬性逐一拷貝至新建對象中數組

function shallowCopy(source) {
    const obj = {}
    for (let i in source) {
        obj[i] = source[i]
    }
    return obj
}

測試一下函數

var obj = {
    a: 1,
    b: [1, 2, 3],
    c: {c1: 4}
}

var obj1 = shallowCopy(obj)
obj1.a = 11
obj1.b[0] = 11
obj1.c.c1 = 44
obj.a // 1
obj.b[0] // 11
obj.c.c1 // 44

測試結果
淺拷貝適用於對象中全部屬性都爲基礎數據類型時,若是源對象包含有引用類型的屬性,則淺拷貝沒法切斷與源對象的關係
es6提供了Object.assign,功能與之相似
還有展開運算符: var obj1 = {...obj}性能

  • 方法二

針對方法一的問題,提出新的方法:
利用JSON.stringify + JSON.parse 進行對象的序列化/反序列化進行對象的拷貝測試

var obj1 = JSON.parse(JSON.stringify(obj))
obj1.a = 11
obj1.b[0] = 11
obj1.c.c1 = 44
obj.a // 1
obj.b[0] // 1
obj.c.c1 // 4

方法二彷佛完美得解決了方法一遺留的問題,再進行測試優化

function Person(age) {
    this.age = age
}
var p = new Person(3)
var obj = {
    a: new Date(),
    b: /abc/igmuy,
    c() {},
    d: new Array(2),
    e: p,
}
var obj1 = JSON.parse(JSON.stringify(obj))
obj1.a // '2019-12-18T08:40:21.650Z' 實際上和調用Date對象的toISOString方法一致,返回的爲時間字符串
obj1.b // {} RegExp對象不會被正確拷貝
obj1.c // undefined 函數不會被正確拷貝
obj1.d // [null, null] 數組空位不會被正確拷貝
obj1.e // obj1.e.constructor指向Object,即原型丟失

還有一種狀況,包含循環引用過的對象使用JSON序列化會拋出異常this

var obj = {}
obj.a = obj
var obj1 = JSON.parse(JSON.stringify(obj))
// 結果會拋出異常

使用JSON序列化/反序列化實現拷貝的侷限性prototype

  1. 沒法對函數、RegExp、Date等特殊對象進行拷貝
  2. 拷貝稀疏數組時會拷貝相同長度但以null填充的數組
  3. 循環引用對象使用該方法會致使異常
  4. 對對象屬性進行拷貝時會丟失對象的原型鏈

以上幾點雖然是該方法的侷限,但一般該方法能知足大多數場景,也不失爲一個簡便的方法code

  • 方法三

針對方法一沒法對引用類型屬性進行拷貝和方法二存在的侷限,能夠針對不一樣狀況,進行鍼對性處理regexp

// 類型判斷方法
const isType = (source, type) => Object.prototype.toString.call(source).slice(8, -1).toLowerCase() === type.toLowerCase()

const deepClone = source => {
    const sources = []
    const children = []
    const _deep = source => {
        if (typeof source !== 'object' ||
            isType(source, 'null') ||
            isType(source, 'undefined')
            ) {return source}
        let child;
        if (isType(source, 'date')) { // 處理date類型
            child = new Date(source.getTime())
        }
        else if (isType(source, 'regexp')) { // 處理正則
            child = new RegExp(source.source, source.flags)
            child.lastIndex = source.lastIndex
        }
        else if (isType(source, 'array')) { // 處理數組
            child = []
        }
        
        else {
            const proto = Object.getPrototypeOf(source)
            child = Object.create(proto)
        }
        
        // 處理循環引用
        const index = sources.indexOf(source)
        if (index >= 0) {
            return children[index]
        }
        
        sources.push(source)
        children.push(child)
        
        for (let i in source) {
            child[i] = _deep(source[i])
        }
        return child
    }
    return _deep(source)
}

該方法解決了前兩個方法中存在的一些問題

  • 方法四

雖然方法三解決了一些問題,但同時引入了一些問題: 因爲使用了遍歷遞歸的方法,在性能方面會有一些問題,有沒有辦法優化呢?

。。。

相關文章
相關標籤/搜索