實現深拷貝的幾種方式


目前使用過四種

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,則為淺拷貝

target Object類型 目標對象,其他對象的成員屬性將被附加到該對象上。

object1  objectN可選。 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(講的比較好,推薦)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM