JS 物件合併與克隆方法的分類與比較

@㍿社長發表於2019-03-04

物件的合併與拷貝(又稱複製或克隆)是前端們平時工作中繞不開的基本操作,使用場景非常多。也許你已經有了自己用慣了的工具方法,但是對於這個話題,你確定自己已經完全瞭解了嗎?

本文詳細分析了js物件的合併與拷貝方法,並試圖從幾個維度對其進行分類和整理。

合併與克隆的關係

首先分析一下合併與克隆的關係。我認為,合併基本上是拷貝的超集

拷貝可以認為是一種特殊情況下的合併:將一個空物件 {} 作為目標,與一個非空物件合併。

但這裡為什麼說“基本上”,而不是“嚴格意義上”呢?那是因為,拷貝有時候會提出一些特殊的要求,而這些要求是普通的合併操作不關注的。舉個例子,拷貝往往會要求目標物件和源物件 constructor 相同,一個 Person 類的例項被拷貝後應該還是一個 Person,不能變成 Dog,更不能變成一個不知道是什麼東西的 Object

此外,合併和拷貝在方法呼叫上也有差別。合併一般要求支援多個源物件向目標物件合併,而拷貝的源物件只有一個。

常見的合併與拷貝的方法

社群以及規範裡也有非常多合併和拷貝方法供碼農們選擇。比如,一個討巧的方式是利用JSON,呼叫JSON.parse(JSON.stringify() )來實現物件複製。ES5 中增加了原生的 Object.assign 來實現合併。而利用 ES6 中的擴充套件運算子,呼叫形如 {…x, …y} 的宣告,也能實現物件的合併。

除了這些原生的合併拷貝方法,我還找了jQuery@3.2.1,underscore@1.8.3,lodash@4.16.1三個大名鼎鼎庫中的相關方法。我們接下來將以他們作為例子,詳細從合併與拷貝方法的各個維度上來分析。

說明:jQuery.extend 方法有很多呼叫方式,既可以拷貝又可以合併,所以在兩個列表中都出現。另外,lodashunderscore 都使用下劃線_,這裡根據“先來後到”的順序,用 _ 指代underscore,用 l 指代 lodash

方法如下。
拷貝方法:
  • JSON.parse(JSON.stringify())
  • $.extend
  • _.clone
  • l.clone
  • l.cloneDeep
  • l.cloneWith
  • l.cloneDeepWith
合併方法:
  • {…x, …y}
  • Object.assign
  • $.extend
  • _.extend
  • _.extendOwn
  • l.assign
  • l.assignIn
  • l.assignWith
  • l.assignInWith
  • l.merge
  • l.mergeWith

拷貝方法分析

本文從這幾個維度來分析拷貝方法:
1.是否支援處理特殊型別?

對於依賴 JSON 來拷貝物件的 JSON.parse(JSON.stringify()) 方法來說,undefinedfunction 型別的屬性會被忽略,而 Date 型別的屬性則會被轉換為字串,這可能不是我們想要的。

2.是否能夠正確處理 constructor?

就像前文所述,Person 不能變為 Dog,也不能變為 Object。目標物件應該保留源物件的 constructor

3.是否是深拷貝?

當源物件的某個屬性為引用型別時,對引用型別的處理決定了這次拷貝操作是深拷貝還是淺拷貝。淺拷貝直接把引用地址原樣拿來,此時,不管源物件還是目標物件,修改引用屬性後另一個物件的同名屬性都會受到影響。深拷貝則會遞迴地在目標物件上建立值,目標物件和源物件之間將完全獨立。

4.是否支援 customizer?

customizer 是指一個處理方法,允許使用者定製拷貝中的處理過程,其作用類似 Array 系列方法中的遍歷處理函式。一開始我也沒想到這個維度,還是在研究 lodash 相關方法的時候才看到的。不得不說,這是一個很有用的特性。

例子:
function customizer(value) {  
    if (_.isElement(value)) {    
        return value.cloneNode(false);  
    }
}
var el = _.cloneWith(document.body, customizer);複製程式碼
現在,根據上面四個維度,我測試了上面列出的拷貝方法,總結成如下表:
方法
是否支援處理特殊型別
是否能夠正確處理constructor
是否是深拷貝
是否支援customizer
JSON.parse(JSON.stringify())
$.extend
支援(第一個引數為true)
_.clone
l.clone
l.cloneDeep
l.cloneWith
l.cloneDeepWith
總結:
  1. underscoreclone 方法不支援深拷貝,比較弱。
  2. jqueryextend 方法預設不使用深拷貝,但當第一個引數傳入 true 時則使用深拷貝來處理。
  3. lodash 提供了4個 clone 相關方法。只有 lodash 的 clone 方法正確處理了 constructor,而 customizer 也只有 lodash 一家獨有(兩個with 方法)。

合併方法分析:

對於合併,本文從以下維度來分析:

1.原型屬性是否參與合併?

原型屬性參與合併時,源物件原型上的屬性會被作為目標物件上的普通屬性。如:

function Foo() {
    this.a = 1;
}
Foo.prototype.b = 2;
let x = new Foo();
assign({}, x);
// {a: 1, b: 2}複製程式碼
2.undefined 值是否參與合併?

字面意思。值 為undefined 的屬性是否參與合併。

3.是否遞迴合併?

遞迴合併的概念與拷貝的 深/淺 相似,但是又有所不同。

首先確認一點,所有的合併操作都不會是“淺”的,都不會直接把引用地址賦給目標物件。但在此基礎上,又有不同的合併策略。比如:

let x = {a: {m: 1, n: 2}};
let y = {a: {m: 2, o: 3}};
assign(x ,y);

// 非遞迴合併
// {a: {m: 2, o: 3}}
// 遞迴合併
// {a: {m: 2 , n: 2, o: 3}}複製程式碼
非遞迴合併會直接用源物件上的值去替換/覆蓋目標物件的值。而遞迴合併則會對其進行進一步的合併。

4.是否支援 customizer?

和上面一樣,允許使用者定製合併的處理過程。
下面對以上合併方法進行測試和分析,結果表如下:
方法
原型屬性是否參與合併
undefined是否參與合併
是否遞迴合併
是否支援customizer
{…x, …y}
Object.assign
jQuery.extend
支援(第一個引數為true)
_.extend
_.extendOwn
l.assign
l.assignIn
l.assignWith
l.assignInWith
l.merge
l.mergeWith
總結:
  1. 原生方法中使用Object.assign方法和使用擴充套件操作符完全一樣。
  2. 除了lodash 的 merge,其餘方法都不支援遞迴合併。
  3. 除了lodash 的 merge,其餘方法undefined都參與合併。
  4. 除了lodash 的三個 with 方法,其餘方法都不支援 customizer

根據上面的兩張表,讀者可以自行選擇合適的合併與拷貝方法了。如果有其他方法,也可以用這些維度來進行分析。

最後,歡迎拍磚~!

相關文章