JavaScript 深拷貝

js深拷貝是一件看起來很簡單的事情,但其實一點兒也不簡單。對於循環引用的問題還有一些內置數據類型的拷貝,如Map, Set, RegExp, Date, ArrayBuffer 和其餘內置類型。處理起來並不是像想象的那麼簡單。下面終結一下在實際的項目中,使用的一些深拷貝的方法的優缺點。你們也能夠 關注個人GitHub,互相交流學習進步。

JSON.parse

先將一個對象轉爲json對象。而後再解析這個json對象。git

let obj = {a:{b:22}};
let copy = JSON.parse(JSON.stringify(obj));

這種方法的優勢就是代碼寫起來比較簡單。可是缺點也是顯而易見的。你先是建立一個臨時的,可能很大的字符串,只是爲了把它從新放回解析器。另外一個缺點是這種方法不能處理循環對象。github

以下面的循環對象用這種方法的時候會拋出異常算法

let a = {};
let b = {a};
a.b = b;
let copy = JSON.parse(JSON.stringify(a));

image

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

let a = {};
let b = new Set();
b.add(11);
a.test = b;
let copy = JSON.parse(JSON.stringify(a));

a 的值打印以下
imageapi

copy的值打印以下
image瀏覽器

對比發現,Set已丟失。異步

Structured Clone 結構化克隆算法

MessageChannel

創建兩個端,一個端發送消息,另外一個端接收消息。post

function structuralClone(obj) {
    return new Promise(resolve =>{
        const {port1, port2} = new MessageChannel();
        port2.onmessage = ev => resolve(ev.data);
        port1.postMessage(obj);
    })
}
const obj = /* ... */;
structuralClone(obj).then(res=>{
     console.log(res);
})

這種方法的優勢就是能解決循環引用的問題,還支持大量的內置數據類型。缺點就是這個方法是異步的。學習

History API

利用history.replaceState。這個api在作單頁面應用的路由時能夠作無刷新的改變url。這個對象使用結構化克隆,並且是同步的。可是咱們須要注意,在單頁面中不要把原有的路由邏輯搞亂了。因此咱們在克隆完一個對象的時候,要恢復路由的原狀。url

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

var obj = {};
var b = {obj};
obj.b = b
var copy = structuralClone(obj); 
console.log(copy);

這個方法的優勢是。能解決循環對象的問題,也支持許多內置類型的克隆。而且是同步的。可是缺點就是有的瀏覽器對調用頻率有限制。好比Safari 30 秒內只容許調用 100 次

Notification API

這個api主要是用於桌面通知的。若是你使用Facebook的時候,你確定會發現時常在瀏覽器的右下角有一個彈窗,對就是這傢伙。咱們也能夠利用這個api實現js對象的深拷貝。

function structuralClone(obj) {
  return new Notification('', {data: obj, silent: true}).data;
}

var obj = {};
var b = {obj};
obj.b = b
var copy = structuralClone(obj);
console.log(copy)

一樣是優勢和缺點並存,優勢就是能夠解決循環對象問題,也支持許多內置類型的克隆,而且是同步的。缺點就是這個須要api的使用須要向用戶請求權限,可是用在這裏克隆數據的時候,不經用戶受權也可使用。在http協議的狀況下會提示你再https的場景下使用。

lodash的_.cloneDeep()

支持循環對象,和大量的內置類型,對不少細節都處理的比較不錯。推薦使用。
支持的類型有不少

image

咱們這裏再次關注一下lodash是如何解決循環應用這個問題的?
image

從相關的代碼中。咱們能夠發現。lodash是用一個棧記錄了。全部被拷貝的引用值。若是再次碰到一樣的引用值的時候,不會再去拷貝一遍。而是利用以前已經拷貝好的值。

lodash深拷貝的詳細的源碼能夠在這裏查看。
https://github.com/lodash/lod...

實現一個簡易點的深拷貝,以解決循環引用的問題爲目標

咱們僅僅實現一個簡易點的深拷貝。能優雅的處理循環引用的便可。在實現深拷貝以前,咱們首先溫習回顧一下js中的遍歷對象的屬性的方法和各類方法的優缺點。

js中遍歷一個對象的屬性的方法

  • Object.keys() 僅僅返回自身的可枚舉屬性,不包括繼承來的,更不包括Symbol屬性
  • Object.getOwnPropertyNames() 返回自身的可枚舉和不可枚舉屬性。可是不包括Symbol屬性
  • Object.getOwnPropertySymbols() 返回自身的Symol屬性
  • for...in 能夠遍歷對象的自身的和繼承的可枚舉屬性,不包含Symbol屬性
  • Reflect.ownkeys() 返回對象自身的全部屬性,不論是否可枚舉,也不論是否是Symbol。注意不包括繼承的屬性

實現深拷貝,解決循環引用問題

/**
 * 判斷是不是基本數據類型
 * @param value 
 */
function isPrimitive(value){
  return (typeof value === 'string' || 
  typeof value === 'number' || 
  typeof value === 'symbol' ||
  typeof value === 'boolean')
}

/**
 * 判斷是不是一個js對象
 * @param value 
 */
function isObject(value){
  return Object.prototype.toString.call(value) === "[object Object]"
}

/**
 * 深拷貝一個值
 * @param value 
 */
function cloneDeep(value){

  // 記錄被拷貝的值,避免循環引用的出現
  let memo = {};

  function baseClone(value){
    let res;
    // 若是是基本數據類型,則直接返回
    if(isPrimitive(value)){
      return value;
    // 若是是引用數據類型,咱們淺拷貝一個新值來代替原來的值
    }else if(Array.isArray(value)){
      res = [...value];
    }else if(isObject(value)){
      res = {...value};
    }

    // 檢測咱們淺拷貝的這個對象的屬性值有沒有是引用數據類型。若是是,則遞歸拷貝
    Reflect.ownKeys(res).forEach(key=>{
      if(typeof res[key] === "object" && res[key]!== null){
        //此處咱們用memo來記錄已經被拷貝過的引用地址。以此來解決循環引用的問題
        if(memo[res[key]]){
          res[key] = memo[res[key]];
        }else{
          memo[res[key]] = res[key];
          res[key] = baseClone(res[key])
        }
      }
    })
    return res;  
  }

  return baseClone(value)
}

驗證咱們寫的cloneDeep是否能解決循環應用的問題

var obj = {};
var b = {obj};
obj.b = b
var copy = cloneDeep(obj); 
console.log(copy);

完美。大功告成

咱們雖然的確解決了深拷貝的大部分問題。不過不少細節尚未去處理。在生產環境,咱們仍是要使用lodash的cloneDeep。cloneDeep對每一個數據類型都單獨處理的很是好。好比ArrayBuffer什麼的。咱們都沒有處理。

相關文章
相關標籤/搜索