摸索 JS 內(nèi)深拷貝的最佳實(shí)踐

問題

由于 js 的傳參方式有時會遇到這樣的場景:

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 } }

我只是想繼承參數(shù)的部分?jǐn)?shù)據(jù),并在此基礎(chǔ)添加一些東西,但是參數(shù) data 的源數(shù)據(jù)也被我改動了,如果之后有其他人想要從data獲取數(shù)據(jù),他可能還需要注意是否有像 setTime 這樣的函數(shù)調(diào)用它。

一點(diǎn)修改

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下每個屬性的值?一個個復(fù)制過來?我們看看 lodash 是怎么做的

lodash 的深拷貝

你猜的沒錯,的確是要深度遍歷的。
baseClone方法內(nèi),拿到要拷貝的對象 value 后,先檢查其類型,然后由對應(yīng)的 handler 來處理,比如value是數(shù)組類型,則使 result 為同樣長度的數(shù)據(jù),然后對每一項(xiàng)都遞歸調(diào)用 baseClone,直到 value 是非引用類型,返回 value的值;如果是普通對象類型,則使 result 為空數(shù)組,然后拿取valuekey,對每個key的賦值也是遞歸調(diào)用baseClone。

想要簡單點(diǎn)

難道我深拷貝一個變量還要引入 lodash 這么麻煩嗎 ?沒有簡單點(diǎn)的辦法嗎?

JSON.parse(JSON.stringify(param))

嗯,可能有點(diǎn)不是那么酷炫,但是他確實(shí)可以滿足要求,而且也無須引入其他的庫。但如果它真的這么完美,為什么 lodash 不這么寫呢?
的確,它的缺點(diǎn)還挺多的,這里取幾個我覺得比較重要的:

  1. Set 類型、Map 類型以及 Buffer 類型會被轉(zhuǎn)換成 {}
  2. undefined、任意的函數(shù)以及 symbol 值,在序列化過程中會被忽略(出現(xiàn)在非數(shù)組對象的屬性值中時)或者被轉(zhuǎn)換成 null(出現(xiàn)在數(shù)組中時)
  3. 對包含循環(huán)引用的對象(對象之間相互引用,形成無限循環(huán))執(zhí)行此方法,會拋出錯誤
  4. 所有以 symbol 為屬性鍵的屬性都會被完全忽略掉,即便 replacer 參數(shù)中強(qiáng)制指定包含了它們

是啊,畢竟JSON的兩個方法本身就只是用來轉(zhuǎn)換 js 內(nèi)的對象為 JSON 格式的,上述幾點(diǎn)甚至都不是缺點(diǎn),是我們想借用其他方法做深拷貝時遇到的問題。

既然是問題那應(yīng)該可以解決吧,比如第一條和第二條,在 stringify 時判斷類型,轉(zhuǎn)化成 帶類型標(biāo)識符的對象字符串如:Set [1,2,3,4,5],然后在parse的時候?qū)ψ址M(jìn)行解析,特別的類型調(diào)用對應(yīng)的構(gòu)造函數(shù)... 聽起來變得更麻煩了,沒關(guān)系,忍忍把各個類型的處理都寫了;針對第三條,拋錯了?沒關(guān)系,我 try catch 包起來...,什么?循環(huán)引用?

循環(huán)引用?

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 的方法來進(jìn)行深拷貝的話,會報循環(huán)結(jié)構(gòu)轉(zhuǎn)換轉(zhuǎn)換 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 分別是是一次遍歷中 要拷貝的值 和 拷貝的結(jié)果。stack 是一個用來儲存每次對應(yīng)的 valueresult 的對象, stack下有一塊用于儲存的數(shù)組結(jié)構(gòu),該數(shù)組的每一項(xiàng)記錄了單次遍歷中的 valueresult,后二者再次以數(shù)組的形式存儲,以 value 做為下標(biāo) 0 的項(xiàng),result 為下標(biāo) 1 的項(xiàng)(這里不用對象的 key-value 形式可能是因?yàn)檠h(huán)引用的變量無法使用 JSON.stringify 轉(zhuǎn)換成字符串,只能 toString 轉(zhuǎn)成 object Object);stack 是做為參數(shù)貫穿整個遍歷過程的,每次遍歷時都會以當(dāng)前的 value 值進(jìn)行查找(這里的查找直接是判斷內(nèi)存地址相等),如果能在 stack 中查到到對應(yīng)的結(jié)果,則直接返回記錄中的result,不再繼續(xù)遞歸。
好了,循環(huán)引用的問題我們解決了,鼓掌!但是我也放棄使用 JSON 方法了...還有沒有其他直接點(diǎn)的方法呢?

其他方法

結(jié)構(gòu)化克隆算法是由HTML5規(guī)范定義的用于復(fù)制復(fù)雜JavaScript對象的算法,它通過遞歸輸入對象來構(gòu)建克隆,同時保持先前訪問過的引用的映射,以避免無限遍歷循環(huán)。

怎么用?
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 原有的狀態(tài),所以用完就要恢復(fù)原狀,當(dāng)無事發(fā)生過。
至少,這是個同步的方法...,如果是同步的場景可以考慮一下...

性能展示

這里的測試代碼是使用的 [Deep-copying in JavaScript] (https://dassur.ma/things/deep-copy/) 一文中的,并再次基礎(chǔ)做了一些修改。

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

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

  • chrome


    chrome
  • safari
    ...em...Safari瀏覽器在調(diào)用完 postMessage 方法后就...沒有然后了...表格都沒刷出來...等了 40 s 終于刷出第一欄...
    注釋完 postMessage 又發(fā)現(xiàn)不能頻繁的調(diào)用 history 。

    調(diào)用 history 的 api 拋異常

safari 結(jié)果
  • firefox
    ...em.. 調(diào)用 history 相關(guān) api 對 firefox 好像壓力很大,以至于循環(huán)都有些錯亂...于是注釋了相關(guān)代碼
firefox 結(jié)果

就結(jié)果而言好像看不出什么區(qū)別,可能是我的數(shù)據(jù)不好,大家可以去看看原文,有展示閱讀性更好的圖表,盡管沒有 lodash 就是了。

結(jié)果

回到我們最初的問題,我們只是想深拷貝一個 js 對象,如果只是一個比較"普通"的對象,用JSON的方法簡單又快捷,但是如果這個對象有些“復(fù)雜”,似乎使用 lodash 的方法是比較好的選擇,而且 lodash 連 Structured Clone 算法忽視的 symbol 類型 和 Function 也考慮其中,兼容性也沒問題,也不會在不同的瀏覽器發(fā)生意外的狀況...
lodash 萬歲!lol!!

參考閱讀:
Deep-copying in JavaScript

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容