banner
他山之石

他山之石

深拷貝探秘

本文總結了一些解決深拷貝的常用方法,沒有銀彈,每種方法都有其優劣,使用時要區分其場景。

image

在 JavaScript 中,深拷貝已經是一個老生常談的問題,也無數次被面試官用來考核一個前端的 JS 水平,同時,在開發中,如果對深拷貝理解不夠深刻,也會出現很難發現的 BUG。此前,我已經寫過一篇文章《深入理解 JavaScript 之克隆》,其中分析了深拷貝的概念,但是其解決方法還很粗淺,這篇文章,記錄了一些解決深拷貝的常用方法,沒有銀彈,每種方法都有其優劣,使用時要區分其場景。

递归拷贝#

递归拷贝應該是最開始了解深拷貝時遇到的解決辦法,其原理是對引用類型進行遍歷,判斷其值是否為引用類型,如果為引用類型則遞歸調用該函數並傳入該值,否則為簡單數據類型則執行常規賦值拷貝。

function deepClone(obj) {
  var result;
  if (!(obj instanceof Object)) {
    return obj;
  } else if (obj instanceof Function) {
    result = eval(obj.toString());
  } else {
    result = Array.isArray(obj) ? [] : {};
  }
  for (let key in obj) {
    result[key] = deepClone(obj[key]);
  }
  return result;
}

這是一個簡單的深拷貝方法,處理了對象、數組和函數,但是 Date、Regexp、Map 等引用類型都沒有處理,而且無法解決對象循環引用的場景。

JSON.parse(JSON.stringify(object))#

深拷貝一般用 JSON.parse (JSON.stringify (object)) 就可以解決了,

這種方法的局限性:

  • 會忽略 undefined
  • 不能序列化函數
  • 不能解決循環引用的對象
    undefined 和函數會被忽略,而嘗試拷貝循環引用的對象則會報錯:
Uncaught TypeError:Converting circular structure to JSON

另外,諸如 Map, Set, RegExp, Date, ArrayBuffer 和其他內置類型在進行序列化時會丟失。

但是在通常情況下,複雜數據都是可以序列化的,所以這個函數可以解決大部分問題,並且該函數是內置函數中處理深拷貝性能最快的。

Structured Clone 结构化克隆算法#

Structured cloning 是一種現有的算法,用於將值從一個地方轉移到另一地方。例如,每當您調用 postMessage 將消息發送到另一個窗口或 WebWorker 時,都会使用它。關於結構化克隆的好處在於它處理循環對象並支持大量的內置類型。結構化克隆包含使用 MessageChannel 和 History API。

MessageChannel#

MessageChannel API 允許我們創建一個新的消息通道,並通過它的兩個 MessagePort 屬性發送數據。
我們可以創建一個 MessageChannel 並發送消息。在接收端,消息包含我們原始數據對象的結構化克隆。
MessageChannel 的 postMessage 傳遞的數據也是深拷貝的,這和 web worker 的 postMessage 一樣。而且還可以拷貝 undefined 和循環引用的對象。

// 有undefined + 循環引用
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3
  },
  f: undefined
};
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;

function deepCopy(obj) {
  return new Promise(resolve => {
    const { port1, port2 } = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

// MessageChannel是異步的
deepCopy(obj).then(copy => {
  let copyObj = copy;
  console.log(copyObj, obj);
  console.log(copyObj == obj);
});

但拷貝有函數的對象時,還是會報錯:

Uncaught (in promise) DOMException:Failed to execute 'postMessage' on 'MessagePort': function() {} could not be cloned.

這種方法的缺點是它是異步的,但是你可以使用 await 來解決。

const clone = await structuralClone(obj);

History#

如果你曾經使用 history.pushState () 寫過 SPA,你可以提供一個狀態對象來保存 URL。事實證明,這個狀態對象使用結構化克隆,而且是同步的。我們必須小心使用,不要把原有的路由狀態搞亂了,所以我們需要在完成克隆之後恢復原始狀態。為了防止發生任何意外,請使用 history.replaceState () 而不是 history.pushState ()。

const structuralClone = obj => {
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
};

此方法的優點是:能解決循環對象的問題,並且支持許多內置類型的克隆,也是同步的。缺點是有的瀏覽器對調用頻率有限制。比如 Safari 30 秒內只允許調用 100 次

Notification#

Notification 是用於桌面通知的。也可以用來實現 JS 對象的深拷貝。

function structuralClone(obj) {
    return new Notification('', {data: obj, silent: true}).data;
}
const obj = /* ... */;
const clone = structuralClone(obj);

此方法的優點就是可以解決循環對象問題,也支持許多內置類型的克隆,並且是同步的。缺點就是這個 api 的使用需要向用戶請求權限,但是用在這裡克隆數據的時候,不經用戶授權也可以使用。在 http 協議的情況下會提示你再 https 的場景下使用。
由於某種原因,Safari 總是返回 undefined。

lodash 的深拷貝函數#

lodash 的_.cloneDeep () 支持循環對象,和大量的內置類型,對很多細節都處理的比較不錯,推薦使用。

解決循環引用#

通過分析上述的方法,發現只有結構化克隆算法才能解決循環引用的問題,那麼使用原生 JS 如何解決呢?這裡嘗試使用閉包來解決循環問題。

function deepClone(object) {
  const memo = {};
  function clone(obj) {
    var result;
    if (!(obj instanceof Object)) {
      return obj;
    } else if (obj instanceof Function) {
      result = eval(obj.toString());
    } else {
      result = Array.isArray(obj) ? [] : {};
    }
    for (let key in obj) {
      if (memo[obj[key]]) {
        result[key] = memo[obj[key]];
      } else {
        memo[obj[key]] = obj[key];
        result[key] = clone(obj[key]);
      }
    }
    return result;
  }
  return clone(object);
}

var obj = {};
var b = { obj };
obj.b = b;

var obj2 = deepClone(obj);

在上述代碼中,對遞歸拷貝進行了優化,使用閉包來解決循環拷貝的問題,使用了 memo 這個變量來記錄被拷貝的引用地址,在每次遞歸前,記錄遞歸的參數值,這樣在遞歸內部就可以跳出遞歸,解決循環引用問題。

總結#

  • 如果您沒有循環對象,並且不需要保留內置類型,則可以使用跨瀏覽器的 JSON.parse (JSON.stringify ()) 獲得最快的克隆性能。
  • 如果你想要一個適當的結構化克隆,MessageChannel 是你唯一可靠的跨瀏覽器的選擇。
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。