你不知道的深複製黑科技

由簡入繁

今天花點時間,來整理一波深複製的skr操作。由簡入繁 實現一個不會循環引用的deepClone方法

深複製&&潛複製

文章開頭,先掃個盲,仔細看下面這個例子

  var obj = {a: 1, b: 2, c: { a: 3 },d: [4, 5]} 
  var obj1 = obj 
  var obj2 = JSON.parse(JSON.stringify(obj))//深拷貝常用方法 
  var obj3 = {...obj} 
  var obj4 = Object.assign({},obj) 
  obj.a = 999 
  obj.c.a = -999 
  obj.d[0] = 123 
  console.log(obj1) //{a: 999, b: 2, c: { a: -999 },d: [123, 5]} 
  console.log(obj2) //{a: 1, b: 2, c: { a: 3 },d: [4, 5]} 
  console.log(obj3) //{a: 1, b: 2, c: { a: -999 },d: [123, 5]} 
  console.log(obj4) //{a: 1, b: 2, c: { a: -999 },d: [123, 5]}

發現什麼問題了嗎?這些操作好像只是遍歷了樹的第一層,然後將指針指向了新的對象

  • 這裏有個知識點:有人可能會覺得基本類型 是在棧中開闢一個新空間存放,其實obj.a 和obj1.a 開始用的是同一個‘1’,後面進行obj.a=111操作的時候 開闢一個新的空間存放999,obj1.a還是存放‘1’

單純可愛001

    以下代碼可能會忽略類型校驗、各種兼容,只要看思路就好0.0

  // 處理對象 var obj = { a: {b: {c: 123}} }
  function clone1 (obj) {
  	var result = {}
    for(var i in obj) {
        if (obj.hasOwnProperty(i)) {
            if (typeof obj[i] === 'object') {
				result[i] = clone(obj[i])
            } else {
                result[i] = obj[i]
            }
        }
    }
    return result
  }
  • 這裏的類型判斷有點問題,完整版本看這裏
  • 想知道爲什麼用hasOwnProperty,請戳這裏
  • 其實還可以思考下,如果兼容數組 要怎麼做,如果是es6的 還會有set、map、weakset、weakmap

上面這這種方法 長相醜陋,而且在深度優先或者循環引用的時候 容易棧溢出(貌似廣度優先不會),所以我們要想個辦法
暫時思路是:破壞引用或者做循環引用檢查,我們接下來會重點介紹前者。

簡單粗暴002

項目中,很多人會通過JSON提供的兩個方法來實現深複製

  JSON.parse(JSON.stringify(obj))
  var a = {}
  a.a = a
  JSON.parse(JSON.stringify(a)) // Uncaught TypeError: Converting circular structure to JSON

但是通過測試發現,複製深度是1000000的對象,直接棧溢出了,說明底層也是使用的遞歸的方式。而且還一個有趣的現象,JSON內部做了循環引用檢查

  • JSON不可以複製對象的方法,莫慌 猛戳這裏找黑科技

高冷帥氣003

想要破解遞歸爆棧,方法只有一個:不用遞歸。我們可以嘗試下上學時候老師教的鏈表還有堆棧
我們把一個對象旋轉90°,像極了一棵樹,那麼我們面對的問題就成了 複製一個樹
鏈表太複雜了,還有記錄指針,我們可以通過棧+js對象這個技巧,規避掉還原樹的時候 找不到節點順序關係的難題,假裝看不到註釋的代碼

function deepClone (obj) {
  // const store = []
  let root = Array.isArray(obj) ? [] : {}

  const loopList = [{parent: root, key: undefined, data: obj}]

  while (loopList.length) {
    // 深度優先 廣度優先沒影響吧
    const node = loopList.pop()
    const parent = node.parent
    const key = node.key
    const data = node.data

    let resource = parent
    if (typeof key !== 'undefined') {
	  // 如果只是對象 這裏可以使用連續賦值
      resource = parent[key] = Array.isArray(data) ? [] : {}
    }

    // let exitedData = find(store, data)
    // if (exitedData) {
    //   parent[key] = exitedData.target
    //   continue
    // }

    // store.push({ data, target: resource })

    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === 'object') {
          loopList.push({parent: resource, key: k, data: data[k]})
        } else {
          resource[k] = data[k]
        }
      }
    }
  }
  return root
}

var O = {
  aaa: {bbb: {ccc: 456},},
  ddd: [123, 456, 789]
}

var A = [
  {aaa: {bbb: {ccc: 456},}},
  132
]

var ooo = deepClone(O)
var aaa = deepClone(A)

目前情況是:遞歸沒有了,但還是沒有破除循環引用。最蛋疼的是還出現了一個新的問題,關係丟失,具體如下

var a = {}
var b = { c: a, d: a}
var e = clone(b)
e.c === e.d // false

然後我們想到,可不可以找一個store做緩存,一來可以打破循環引用,二來 還可以空間換時間 騷秀一波
打開註釋,見證奇蹟的時刻到了 skr skr skr
騷完之後,我們迴歸現實 看看這個方法的缺點:

  • 如果你不想保持對象的關聯關係,不要用此方法
  • 如果對象數量很多,不要用此方法

附錄

Types方法

var type = (param) => {
  var checker = Object.prototype.toString
  var getName = () => {
    return checker.call(param).slice(8).slice(0, -1).toLowerCase()
  }
  var is = (typeName) => {
    return checker.call(param) === `[object ${typeName}]`
  }
  var isOneOf = (names = []) => {
    return names.some(name => checker.call(param) === `[object ${name}]`)
  }
  var isEvery = (names = []) => {
    return names.every(name => checker.call(param) === `[object ${name}]`)
  }
  var isNot = (typeNames = []) => {
    var checkers = {
      string: () => !is(typeNames),
      array: () => typeNames.every(name => !is(name))
    }
    return checkers[getName(typeNames)]()
  }
  // 暫時閉包夠用了,後面用涉及到this了 記得bind
  return { is, isOneOf, isEvery, isNot, name: getName() }
}

for-in、Object.keys、Object.getOwnPropertyNames的用法

  • for-in:輸出原型鏈+自身所有可枚舉的屬性
  • keys:輸出自身可枚舉屬性,就是for-in + hasOwnProperty
  • getOwnPropertyNames:輸出自身全部屬性

測試深度優先+廣度優先

function dataAction(deep, breadth) {
    var data = {}, pointer = data
    for (var i = 0; i < deep; i++) {
        pointer = pointer['data'] = {}
        for (var j = 0; j < breadth; j++) {
            pointer[j] = j
        }
    }
    return data
}
// test
clone1(dataAction(n,m))

JSON黑科技

// 這個 parse 比較牛逼 可以不丟失function
// 注意:不要搞太大的對象 storage最大5M
const JSONBooster = {
  stringify: (toJson) => {
    return JSON.stringify(toJson, function (key, val) {
      if (typeof val === 'function') { return `0?0:${val}` }
      return val
    })
  },
  parse: (str) => {
    return JSON.parse(str, function (k, v) {
      if (v.indexOf && v.indexOf('function') > -1) {
        return evil(`(function(){return ${v} })()`)
      }
      return v
    })
  }
}

// eval的代替方案 貌似可行
const evil = (fn) => {
  var Fn = Function
  return new Fn('return ' + fn)()
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章