怎麼實現javascript對象的深拷貝

文章首發於我的博客javascript

前提

在處理平常的業務開發當中,數據拷貝是常常須要用到的。可是 javascript 提供的數據操做 Api 當中能實現對象克隆的都是淺拷貝,好比 Object.assign 和 ES6 新增的對象擴展運算符(...),這兩個 Api 只能實現對象屬性的一層拷貝,對於複製的屬性其值若是是引用類型的狀況下,拷貝事後的新對象仍是會保留對它們的引用。java

簡單粗暴的深拷貝

ESMAScript 給咱們提供了關於操做 JSON 數據的兩個 APi,經過把 javascript 對象先轉換爲 JSON 數據,以後再把 JSON 數據轉換爲 Javascript 對象,這樣就簡單粗暴的實現了一個 javascript 對象的深拷貝,不論這個對象層級有多深,都會徹底與源對象沒有任何聯繫,切斷其屬性的引用,如今看一下這兩個 API 用代碼是怎麼實現的。git

// 現建立一個具備深度嵌套的源對象
const sourceObj = {
  nested: {
    name: 'KongLingWen',
  },
}

// 把源對象轉化爲JSON格式的字符串

const jsonTarget = JSON.stringify(sourceObj)

// 以解析json數據的方式轉換爲javascript對象
let target
try {
  target = JSON.parse(jsonTarget) //數據若是不符合JSON對象格式會拋錯,這裏加一個錯誤處理
} catch (err) {
  console.error(err)
}

target.nested.name = ''

console.log(soruceObj.nested.name) //KongLingWen

代碼最後經過更改新拷貝對象的 name 屬性 ,輸出源對象此屬性的值不變,這說明咱們以這種方式就實現了一個對象深拷貝。github

JSON.parse 和 JSON.stringify 轉換屬性值先後的不一致性

  • 函數沒法序列化函數,屬性值爲函數的屬性轉換以後丟失
  • 日期 Date 對象
    javascript Date 對象轉換到 JSON 對象以後沒法反解析爲 原對象類型,解析後的值仍然是 JSON 格式的字符串
  • 正則 RegExp 對象
    RegExp 對象序列化後爲一個普通的 javascript 對象,一樣不符合預期
  • undefined
    序列化以後直接被過濾掉,丟失拷貝的屬性
  • NaN
    序列化以後爲 null,一樣不符合預期結果

此方式拷貝對象由於有以上這麼多缺陷,因此咱們不如本身封裝一個屬於本身的 javascript 對象深拷貝的函數,反而一勞永逸。json

手動封裝對象深拷貝方法

對象屬性的拷貝無疑就是把源對象的屬性以深度遍歷的方式複製到新的對象上,當遍歷到一個屬性值爲對象類型的值時,就須要針對這個值進行再次的遍歷,也是就用遞歸的方式遍歷源對象的全部屬性。讓咱們先看這一部分代碼函數

function cloneDeep(obj) {
  const result = {}
  for (let key in obj) {
    // 判斷key 是不是對象自身上的屬性,以免對象原型鏈上屬性的拷貝
    if (obj.hasOwnProperty(key)) {
      result[key] = cloneDeep(obj[key]) //須要對屬性值遞歸拷貝
    }
  }
}

這段代碼是對象屬性深拷貝的邏輯,可是不一樣的數據類型各自有其特殊的操做方式須要處理,下面就把這些處理邊界場景的代碼補充上,看看完成的代碼應該是怎樣的編碼

function isPrimitiveValue(value) {
  if (
    typeof value === 'string' ||
    typeof value === 'number' ||
    value == null ||
    typeof value === 'boolean' ||
    Number.isNaN(value)
  ) {
    return true
  }

  return false
}

function cloneDeep(value) {
  // 判斷拷貝的數據類型,若是爲原始類型數據,直接返回其值

  if (isPrimitiveValue(value)) {
    return value
  }
  // 定義一個保存引用類型的變量,根據 引用數據類型不一樣的子類型初始化不一樣的值,如下對象類型的判斷和初始化能夠根據自身功能的須要作刪減。這裏列出了全部的引用類型的場景。
  let result

  if (typeof value === 'function') {
    // result=value 若是複製函數的時候須要保持同一個引用能夠省去新函數的建立,這裏用eval建立了一個原函數的副本
    result = eval(`(${value.toString()})`)
  } else if (Array.isArray(value)) {
    result = []
  } else if (value instanceof RegExp) {
    result = new RegExp(value)
  } else if (value instanceof Date) {
    result = new Date(value)
  } else if (value instanceof Number) {
    result = new Number(value)
  } else if (value instanceof String) {
    result = new String(value)
  } else if (value instanceof Boolean) {
    result = new Boolean(value)
  } else if (typeof value === 'object') {
    result = new Object()
  }

  for (let key in value) {
    if (value.hasOwnProperty(key)) {
      try {
        result[key] = cloneObject(value[key]) //屬性值爲原始類型包裝對象的時候,(Number,String,Boolean)這裏會拋錯,須要加一個錯誤處理,對運行結果沒有影響。
      } catch (error) {
        // console.error(error)
      }
    }
  }

  return result
}

代碼中首先封裝了一個判斷數據是不是原始類型的方法,這裏只是爲了保持 cloneDeep 函數的功能幹淨,其實你也能夠徹底放到一塊,這個徹底取決於本身的編碼風格。若是在業務上須要有多處判斷數據是原始類型仍是引用類型的場景時,以上這種代碼功能抽離的方式就方便處理了。code

查看更多 Javascript 函數功能封裝在 Github 倉庫狠狠的點擊這裏對象

備註:本片博文爲做者原創,轉載請標註出處,謝謝!遞歸

做者:孔令文

相關文章
相關標籤/搜索