ES6時代,你真的會克隆物件嗎(二)

Alvin_Liu發表於2018-02-05

原文:ES6時代,你真的會克隆物件嗎(二)

上一篇,我們從Symbol和是否可列舉以及屬性描述符的角度分析了ES6下怎麼淺拷貝一個物件,發表在掘金segmentfault上,從評論看,部分人覺著看不懂,今天,我們用更簡單的方式來聊聊深拷貝的問題

寫在前面

深拷貝的話題好像從來沒有停止過討論,JavaScript並沒有一個可以實現深拷貝的方法,我們常見的實現方式是遞迴和JSON.parse(JSON.stringify())(聽說底層還是用了遞迴),然而一般庫函式也只能處理常見的需求(不常見的需求真的存在嗎?真的需要用深拷貝嗎?真的不承認是你程式碼的問題嗎?)。今天,我就仔細、認真,細緻(也不是很細緻),負責(也不敢太保證)的態度來研究一下怎麼實現一個深拷貝吧,雖然一度放棄,事實也的確是放棄了,但不把這麼多天的付出寫出來怎麼對得起那個在這個寒冷的冬天忍住瑟瑟發抖的在鍵盤上敲擊的我...

常見深拷貝

JSON系列化

JSON.parse(JSON.stringify())的確是一種很簡單易用的方式呢,可惜的是,JSON是一個很有原則的男人,他可不會對你言聽計從。在遇到不安全的JSON值會自動將其忽略,在陣列中則會返回null(以保證單元位置不變)。

不安全的 JSON 值: undefined 、 function 、 symbol (ES6+)和包含迴圈引用(物件之間相互引用,形成一個無限迴圈)的 物件 都不符合 JSON 結構標準,支援 JSON 的語言無法處理它們

遞迴

上一篇講淺拷貝的時候,我們在開始引入了一個淺拷貝的例子,現在我們把它改成一件簡單的深拷貝。

function deepCopy (obj) {
  if (typeof obj !== 'object') {
    return
  }
  var newObj = obj instanceof Array ? [] : {}
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]
    }
  }
  return newObj
}
複製程式碼

好像也還不錯,簡單易懂還能用,一般的場景的確是一種不錯的方法呢,但是,今天我們來看看不一般的場景。

我們先來挑挑毛病:

  • function型別沒有處理(大概,或許,應該是真的沒必要吧,下面我也並不打算討論這貨,有興趣的去看看callapplybind
  • 迴圈引用
  • 型別判斷用typeofinstanceof靠譜嗎?(特別注意typeof null的坑)
  • 陣列?[]:{},這麼簡單?不存在的

迴圈引用

上面多處說到了迴圈引用的問題,我們先來看看什麼是迴圈引用:

var a = {}
a.b = a
複製程式碼

是的,就是這麼一個反人類的存在,但是卻是我們不能忽略的一個大問題。我們是應該返回空呢、undefined呢,還是它的引用,還是什麼呢?好像沒有標準答案呢,嗯,那就Follow Your Heart吧!

型別判斷

思考一下:

typeof null  // "object"
null instanceof Object  // false
複製程式碼

進行型別判斷是無可避免的,然而我們似乎並沒有什麼完美的方式得到我們需要的型別,我們先來看看幾種常用的方式:

  • typeof: 返回一個表示式的資料型別的字串,返回結果為js基本的資料型別,包括number,boolean,string,object,undefined,function,symbol
  • instanceof: 判斷一個物件是否為某一資料型別,或一個變數是否為一個物件的例項;返回boolean型別。內建型別只有通過構造器才能用instanceof
  • constructor: 是每一個例項物件都擁有的屬性,而這個屬性也相當於是一個指標,它指向於建立當前物件的物件
  • Object.prototype.toString.call(obj).slice(8,-1): 返回的是類名

typeof的問題就很明顯了:

typeof null  // "object"
typeof function () {}  // "function"
typeof []  // "object"
複製程式碼

instanceof考慮一下多全域性物件(多個frame或多個window之間的互動),在瀏覽器中,我們的指令碼可能需要在多個視窗之間進行互動。多個視窗意味著多個全域性環境,不同的全域性環境擁有不同的全域性物件,從而擁有不同的內建型別建構函式。這可能會引發一些問題。比如,表示式 [] instanceof window.frames[0].Array 會返回false,因為 Array.prototype !== window.frames[0].Array.prototype

constructor屬性得到的僅僅是建構函式,而且是可以被手動更改的,constructor.name只是返回的建構函式的名字,它並不返回類名。

Object.prototype.toString.call算是比較公認靠譜的方法了吧,然而,它同樣有可能被人為仿造,鴨子型別嘛,但它還是比較安全的方式。

鴨子型別: "如果它走起路來像鴨子,叫起來也是鴨子,那麼它就是鴨子"。動態型別的語言傾向於你讓它做什麼它就是什麼

型別分析

討論鋪墊的內容應該夠細了吧,接下來我們看看js的複雜資料型別到底有多複雜。

我們常見的有:

基本包裝型別(Boolean、String、Number)、function、Array、Date

你常見,但你不一定想的起的:

RegExp,Arguments,Error、NodeList

你不一定常見,你也不一定知道的:

Blob、File、FileList、ImageData

ES6:

Map、Set、WeakMap、WeakSet、ArrayBuffer物件、TypedArray檢視和DataView檢視、Float32Array、Float64Array、Int8Array...

或許列舉的少了不少,但是已經夠讓人擔憂深克隆的複雜程度了,一一實現他們不是一件簡單的事情,甚至是一件完全沒有必要的事情(當然可以讓你瞭解更多),推薦幾個很優秀的方案供參考:

  • lodash克隆,lodash花了大量的程式碼來實現 ES6 引入的大量新的標準物件。更厲害的是,lodash 針對存在環的物件的處理也是非常出色的
  • jQuery克隆無法正確深複製 JSON 物件以外的物件
  • 結構化克隆演算法

寫在最後

克隆的部分就寫的差不多了,本來想寫點Map、Set的內容的,無賴,並沒有找到合適的地方,MDN阮一峰的ECMAScript 6 入門都介紹的挺好的。

好吧,就這樣吧,前端界的小學生,不足之處,還請指正

相關文章