js深拷貝最終解決方案

本文將提供javascript深拷貝的解決方案,能處理多重嵌套,對數組、函數、Date、RegExp、null、undefined、原型鏈、循環引用等都有進行處理。

相信大家已經瞭解到JavaScript的深拷貝和淺拷貝概念。對於深拷貝,大家常用JSON“序列化+反序列化”的方式來解決,但是

由於JSON.parse(JSON.stringify())進行深拷貝會有如下問題:

對於 JSON.parse(JSON.stringify(data))

  • 1.data內若屬性的鍵值爲時間對象,其會轉爲日期字符串形式。
  • 2.data內若屬性的鍵值爲RegExp、Error對象,其會轉爲空對象{}
  • 3.data內若屬性的鍵值爲undefined、函數或symbol,其會把該屬性丟失。
  • 4.data內若屬性的鍵值有NaN、Infinity和-Infinity,其會變成null
  • 5.data內若屬性的鍵值由自定義構造函數生成的,轉換後會丟棄對象的constructor。
  • 6.data中若存在循環引用的情況也無法正確實現深拷貝。

情況1-5示例: 

function Person(name) {
  this.name = name;     
}      
Person.prototype.speak = function(){
  console.log('i can speak')
}
var arr1 =  {
  a:1,
  b:undefined,
  c:new Date('2019-12-24'),
  d:new Person('mily'),
  e:new RegExp('\\w+'),
  f:NaN,
  g:Symbol("foo"),
  h:{
    total:12,
    list:[{name:'小明',age:34},{name:'小紅',age:23}]
  }
}
// JSON.parse(JSON.stringify())方式
console.log(JSON.parse(JSON.stringify(arr1))
//結果爲
//{
//   a: 1
//   c: "2019-12-24T00:00:00.000Z"
//   d: {name: "mily"}
//   e: {}
//   f: null
//   h: {total: 12, list: Array(2)}
// }

循環引用示例

var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;//循環引用
console.log(JSON.parse(JSON.stringify(arr1)))

直接就報錯了。 

所以JSON.parse(JSON.stringify(data))並不是萬能的解決方案。不過JSON.parse(JSON.stringify(data))簡單粗暴,已經滿足90%的使用場景了。因此 只能通過"遞歸"來解決上述的問題,函數如下:

/**
 * isType 判斷數據類型
 * @param obj 要檢驗類型的數據,必填
 * @param type 要檢驗的數據類型,選填
 * @returns {String|Boolean} 返回對應數據格式的字符串,或者與type參數比較的布爾值
 */
const isType = function(obj, type) {
  // tostring會返回對應不同的標籤的構造函數
  const toString = Object.prototype.toString
  const map = {
    '[object Boolean]': 'boolean',
    '[object Number]': 'number',
    '[object String]': 'string',
    '[object Function]': 'function',
    '[object Array]': 'array',
    '[object Date]': 'date',
    '[object RegExp]': 'regExp',
    '[object Undefined]': 'undefined',
    '[object Null]': 'null',
    '[object Object]': 'object'
  }
  if (obj instanceof HTMLElement) {
    return type ? type === 'element' : 'element'
  }
  return type ? type === map[toString.call(obj)] : map[toString.call(obj)]
}
/**
 * clone 對象或數組的拷貝函數
 * @param obj 要拷貝的數據(對象/數組)
 * @returns {Object/Array} 返回對象或數組
 */
const clone = function(obj, parent = null) {
  //parent用於遞歸循環引用爆棧處理
  // 創建一個新對象
  let result = new obj.constructor() //保持繼承鏈
  let keys = Object.keys(obj),
    key = null,
    temp = null,
    _parent = parent
  // 該字段若有父級則需要追溯該字段的父級
  while (_parent) {
    // 如果該字段引用了它的父級則爲循環引用
    if (_parent.originalParent === obj) {
      return _parent.currentParent // 循環引用直接返回同級的新對象
    }
    _parent = _parent.parent
  }
  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    temp = obj[key]
    if (temp && isType(temp) === 'date') {
      result[key] = new Date(temp)
      continue
    }
    if (temp && isType(temp) === 'regExp') {
      result[key] = new RegExp(temp)
      continue
    }
    // 若字段的值是一個對象/數組
    if (temp && (isType(temp) === 'object' || isType(temp) === 'array')) {
      // 遞歸執行深拷貝 將同級的待拷貝對象與新對象傳遞給 parent 方便追溯循環引用
      result[key] = clone(temp, {
        originalParent: obj,
        currentParent: result,
        parent: parent
      })
    } else {
      result[key] = temp
    }
  }
  return result
}

還是使用上述例子運行,用戶可以輸出複製前的arr1,和複製後clone(arr1)進行比對,發現數據及格式完全沒有丟失。

console.log(arr1,clone(arr1))

 用戶還可以手動驗證:修改複製後的var copydata = clone(arr1)的某些屬性,發現不會修改到arr1。真正實現了深拷貝。

上面的isType()函數還可以用來判斷數據的類型,大家可以放在公共函數內積極使用!

初次練手,如果有錯誤及優化的地方請積極指正。

發佈了2 篇原創文章 · 獲贊 6 · 訪問量 1652
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章