摸索 JS 內深拷貝的最佳實踐

問題

因爲 js 的傳參方式有時會遇到這樣的場景:html

function setTime(data) {
  let result = {};
  result.obj = data.obj || {};
  result.obj.time = Date.now();
  return result
}

let data = {
  title:'loooook!',
  obj: {
    name: 'keo',
    age: '12'
  }
}

let res = setTime(data);

console.log('res',res);
//res { obj: { name: 'keo', age: '12', time: 1533625350183 } }
console.log('data',data);
//data { title: 'loooook!', obj: { name: 'keo', age: '12', time: 1533625350183 } }

我只是想繼承參數的部分數據,並在此基礎添加一些東西,可是參數 data 的源數據也被我改動了,若是以後有其餘人想要從data獲取數據,他可能還須要注意是否有像 setTime 這樣的函數調用它。算法

一點修改

function setTime(data) {
  let result = {};
  result.obj =  {};
  Object.assign(result.obj,data.obj)
  result.obj.time = Date.now();
  return result
}

嗯,或者你也能夠用 for...in,注意下兩者的不一樣。
咱們知道 Object.assign 只是淺拷貝,若是 data.obj 的屬性值仍然有引用類型的話,那麼仍是會碰見一樣的問題。
那要怎麼辦?難道要遍歷data下每一個屬性的值?一個個複製過來?咱們看看 lodash 是怎麼作的
lodash 的深拷貝
你猜的沒錯,的確是要深度遍歷的。
baseClone方法內,拿到要拷貝的對象 value 後,先檢查其類型,而後由對應的 handler 來處理,好比value是數組類型,則使 result 爲一樣長度的數據,而後對每一項都遞歸調用 baseClone,直到 value 是非引用類型,返回 value的值;若是是普通對象類型,則使 result 爲空數組,而後拿取valuekey,對每一個key的賦值也是遞歸調用baseClonechrome

想要簡單點

難道我深拷貝一個變量還要引入 lodash 這麼麻煩嗎 ?沒有簡單點的辦法嗎?api

JSON.parse(JSON.stringify(param))

嗯,可能有點不是那麼酷炫,可是他確實能夠知足要求,並且也無須引入其餘的庫。但若是它真的這麼完美,爲何 lodash 不這麼寫呢?
的確,它的缺點還挺多的,這裏取幾個我以爲比較重要的:數組

  1. Set 類型、Map 類型以及 Buffer 類型會被轉換成 {}
  2. undefined、任意的函數以及 symbol 值,在序列化過程當中會被忽略(出如今非數組對象的屬性值中時)或者被轉換成 null(出如今數組中時)
  3. 對包含循環引用的對象(對象之間相互引用,造成無限循環)執行此方法,會拋出錯誤
  4. 全部以 symbol 爲屬性鍵的屬性都會被徹底忽略掉,即使 replacer 參數中強制指定包含了它們

是啊,畢竟JSON的兩個方法自己就只是用來轉換 js 內的對象爲 JSON 格式的,上述幾點甚至都不是缺點,是咱們想借用其餘方法作深拷貝時遇到的問題。瀏覽器

既然是問題那應該能夠解決吧,好比第一條和第二條,在 stringify 時判斷類型,轉化成 帶類型標識符的對象字符串如:Set [1,2,3,4,5],而後在parse的時候對字符串進行解析,特別的類型調用對應的構造函數... 聽起來變得更麻煩了,不要緊,忍忍把各個類型的處理都寫了;針對第三條,拋錯了?不要緊,我 try catch 包起來...,什麼?循環引用?異步

循環引用?

function parse (param){
  return JSON.parse(JSON.stringify(param))
}

var a = {}
var b = {}
a['b'] = b
b['a'] = a

console.log(parse(a))
//TypeError: Converting circular structure to JSON at JSON.stringify

如上代碼, 變量ab 互相引用對方,此時若是借用 JSON 的方法來進行深拷貝的話,會報循環結構轉換轉換 JSON 錯誤。這個問題怎麼解決呢?咱們再翻出 lodash 的源碼看看...函數

// Check for circular references and return its corresponding clone.
      stack || (stack = new Stack);
      var stacked = stack.get(value);
      if (stacked) {
        return stacked;
      }
      stack.set(value, result);

這裏的 valueresult 分別是是一次遍歷中 要拷貝的值 和 拷貝的結果。stack 是一個用來儲存每次對應的 valueresult 的對象, stack下有一塊用於儲存的數組結構,該數組的每一項記錄了單次遍歷中的 valueresult,後兩者再次以數組的形式存儲,以 value 作爲下標 0 的項,result 爲下標 1 的項(這裏不用對象的 key-value 形式多是由於循環引用的變量沒法使用 JSON.stringify 轉換成字符串,只能 toString 轉成 object Object);stack 是作爲參數貫穿整個遍歷過程的,每次遍歷時都會以當前的 value 值進行查找(這裏的查找直接是判斷內存地址相等),若是能在 stack 中查到到對應的結果,則直接返回記錄中的result,再也不繼續遞歸。
好了,循環引用的問題咱們解決了,鼓掌!可是我也放棄使用 JSON 方法了...還有沒有其餘直接點的方法呢?post

其餘方法

結構化克隆算法是由HTML5規範定義的用於複製複雜JavaScript對象的算法,它經過遞歸輸入對象來構建克隆,同時保持先前訪問過的引用的映射,以免無限遍歷循環。性能

怎麼用?
emmm... 它還不能直接使用,你得依靠一些其餘的 API ,間接的使用它。

  • postMessage()
function StructuredClone(param) {
  return new Promise(function (res, rej) {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => res(ev.data);
    port1.postMessage(param);
  })
}

StructuredClone(objects).then(result => console.log(result))

什麼??仍是異步的... 不,我但願能使用同步的方法使用它。

  • history()
function structuralClone(obj) {
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
}
const clone = structuralClone(objects);

如你所見,咱們要借用一下 history.replaceState 這個方法,可是咱們不能改變 history 原有的狀態,因此用完就要恢復原狀,當無事發生過。
至少,這是個同步的方法...,若是是同步的場景能夠考慮一下...

性能展現

這裏的測試代碼是使用的 [Deep-copying in JavaScript] (https://dassur.ma/things/deep... 一文中的,並再次基礎作了一些修改。

結果! (很懶就不畫圖表了)

單位 μs (繆斯),計算時間的用的接口是 performance.now()結果精確到5微秒。

  • chrome

chrome

  • safari

...em...Safari瀏覽器在調用完 postMessage 方法後就...沒有而後了...表格都沒刷出來...等了 40 s 終於刷出第一欄...
註釋完 postMessage 又發現不能頻繁的調用 history 。
調用 history 的 api 拋異常

safari 結果

  • firefox

...em.. 調用 history 相關 api 對 firefox 好像壓力很大,以致於循環都有些錯亂...因而註釋了相關代碼

 firefox 結果

就結果而言好像看不出什麼區別,多是個人數據很差,你們能夠去看看原文,有展現閱讀性更好的圖表,儘管沒有 lodash 就是了。

結果

回到咱們最初的問題,咱們只是想深拷貝一個 js 對象,若是隻是一個比較"普通"的對象,用JSON的方法簡單又快捷,可是若是這個對象有些「複雜」,彷佛使用 lodash 的方法是比較好的選擇,並且 lodash 連 Structured Clone 算法忽視的 symbol 類型 和 Function 也考慮其中,兼容性也沒問題,也不會在不一樣的瀏覽器發生意外的情況...
lodash 萬歲!lol!!

參考閱讀:
Deep-copying in JavaScript

相關文章
相關標籤/搜索