1. 程式人生 > 實用技巧 >實現深拷貝的幾種方式

實現深拷貝的幾種方式

目前使用過四種

1、遞迴遞迴去複製所有層級屬性

function deepClone(obj){
    let objClone = Array.isArray(obj)?[]:{};
    if(obj && typeof obj==="object"){
        for(key in obj){
            if(obj.hasOwnProperty(key)){
                //判斷ojb子元素是否為物件,如果是,遞迴複製
                if(obj[key]&&typeof obj[key] ==="object"){
                    objClone[key] 
= deepClone(obj[key]); }else{ //如果不是,簡單複製 objClone[key] = obj[key]; } } } } return objClone; } let a=[1,2,3,4], b=deepClone(a); a[0]=2; console.log(a,b);

跟之前想象的一樣,現在b脫離了a的控制,不再受a影響了。

這裡再次強調,深拷貝,是拷貝物件各個層級的屬性,可以看個例子。JQ裡有一個extend方法也可以拷貝物件,我們來看看

let a=[1,2,3,4],
    b=a.slice();
a[0]=2;
console.log(a,b);

那是不是說slice方法也是深拷貝了,畢竟b也沒受a的影響,上面說了,深拷貝是會拷貝所有層級的屬性,還是這個例子,我們把a改改

let a=[0,1,[2,3],4],
        b=a.slice();
a[0]=1; a[2][0]=1; console.log(a,b);

拷貝的不徹底啊,b物件的一級屬性確實不受影響了,但是二級屬性還是沒能拷貝成功,仍然脫離不了a的控制,說明slice根本不是真正的深拷貝。

這裡引用知乎問答裡面的一張圖

第一層的屬性確實深拷貝,擁有了獨立的記憶體,但更深的屬性卻仍然公用了地址,所以才會造成上面的問題。

同理,concat方法與slice也存在這樣的情況,他們都不是真正的深拷貝,這裡需要注意。

2.除了遞迴,我們還可以借用JSON物件的parse和stringify

function deepClone(obj){
    let _obj = JSON.stringify(obj),
        objClone = JSON.parse(_obj);
    return objClone
}    
let a=[0,1,[2,3],4],
    b=deepClone(a);
a[0]=1;
a[2][0]=1;
console.log(a,b);

可以看到,這下b是完全不受a的影響了。

附帶說下,JSON.stringify與JSON.parse除了實現深拷貝,還能結合localStorage實現物件陣列儲存。有興趣可以閱讀博主這篇文章。

localStorage儲存陣列,物件,localStorage,sessionStorage儲存陣列物件

3.除了上面兩種方法之外,我們還可以借用JQ的extend方法。

$.extend( [deep ], target, object1 [, objectN ] )

deep表示是否深拷貝,為true為深拷貝,為false,則為淺拷貝

targetObject型別 目標物件,其他物件的成員屬性將被附加到該物件上。

object1objectN可選。 Object型別 第一個以及第N個被合併的物件。

let a=[0,1,[2,3],4],
    b=$.extend(true,[],a);
a[0]=1;
a[2][0]=1;
console.log(a,b);

可以看到,效果與上面方法一樣,只是需要依賴JQ庫。

說了這麼多,瞭解深拷貝也不僅僅是為了應付面試題,在實際開發中也是非常有用的。例如後臺返回了一堆資料,你需要對這堆資料做操作,但多人開發情況下,你是沒辦法明確這堆資料是否有其它功能也需要使用,直接修改可能會造成隱性問題,深拷貝能幫你更安全安心的去操作資料,根據實際情況來使用深拷貝,大概就是這個意思。

4.lodash的_.cloneDeep()

參考部落格:https://www.cnblogs.com/echolun/p/7889848.html

以下是我參看的一位關於深拷貝的問題解決。

JSON.parse

先將一個物件轉為json物件。然後再解析這個json物件。

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

這種方法的優點就是程式碼寫起來比較簡單。但是缺點也是顯而易見的。你先是建立一個臨時的,可能很大的字串,只是為了把它重新放回解析器。另一個缺點是這種方法不能處理迴圈物件。

如下面的迴圈物件用這種方法的時候會丟擲異常

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

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

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

a 的值列印如下

copy的值列印如下

對比發現,Set已丟失。

Structured Clone 結構化克隆演算法

MessageChannel

建立兩個端,一個端傳送訊息,另一個端接收訊息。

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。這個物件使用結構化克隆,而且是同步的。但是我們需要注意,在單頁面中不要把原有的路由邏輯搞亂了。所以我們在克隆完一個物件的時候,要恢復路由的原狀。

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()

支援迴圈物件,和大量的內建型別,對很多細節都處理的比較不錯。推薦使用。

支援的型別有很多

我們這裡再次關注一下lodash是如何解決迴圈應用這個問題的?

從相關的程式碼中。我們可以發現。lodash是用一個棧記錄了。所有被拷貝的引用值。如果再次碰到同樣的引用值的時候,不會再去拷貝一遍。而是利用之前已經拷貝好的值。

lodash深拷貝的詳細的原始碼可以在這裡檢視。

實現一個簡易點的深拷貝,以解決迴圈引用的問題為目標

我們僅僅實現一個簡易點的深拷貝。能優雅的處理迴圈引用的即可。在實現深拷貝之前,我們首先溫習回顧一下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什麼的。我們都沒有處理。

作者:Jesse
連結:https://www.zhihu.com/question/23031215/answer/460652947
來源:知乎
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

參考部落格:https://www.zhihu.com/question/23031215(講的比較好,推薦)