js 深度合併兩個物件
起因
今天使用 vue 開發元件的時候,使用到了 echart 。
我遇到的問題就是,我有一個基礎樣式,是以物件形式儲存的,名稱是baseStyle
。這個元件對外透露一個 style
的props,型別也規定為物件,預設值為空物件。
然後我希望這兩個物件合併在一起,形成的樣式為總的樣式,衝突的以 style
為主。也就是說,在我有自定義樣式的需求的時候,我能改變樣式,比如:
// 基礎樣式 let baseStyle={ series:[ { name:"選擇", data:[1,2,3] } ] } // 外界引數 let style={ series:[ { name:"我", } ] } // 我希望的最終的樣式 let ans={ series:[ { name:"我", data:[1,2,3] } ] }
尋求解決
一開始是使用的Object.assign()
,發現這種方案是淺複製,同名的物件key會直接覆蓋掉,這不是我想要的結果。
那就只能自己手把手的寫個合併函式,開始想的是遞迴,然後分類處理,但是問題來了:
踩了很多坑,比如使用typeof
判斷型別,我懵了,沒想到陣列也是object
,但是函式就是function
,我尋思 type 不就是型別的意思嗎?(好吧就是我懶,很少用到這個關鍵字。我還以為會出現array
型別,好吧是我typescript用多了)
然後複製是可以複製,但是陣列變成了物件的形式。
後來我在網上尋找思路,我搜索了:js 物件深度複製。
搜尋後期間試驗了幾個方案,都不是很理想。
最後在思否上看到了結局方案,叫去看JQuery的extend原始碼,我一看,我擦,這判斷型別把我整的一愣一愣的(我就是今天在判斷型別上吃了虧),太牛叉了。
但是呢,JQuery 對陣列的處理,是採用合併,而不是和物件一樣,相同的位置進行覆蓋
我以我的需求進行了部分改寫。
解決程式碼
/** * 深度合併程式碼,思路來自 zepto.js 原始碼 * 切記不要物件遞迴引用,否則會陷入遞迴跳不出來,導致堆疊溢位 * 作用是會合並 target 和 other 對應位置的值,衝突的會保留 target 的值 */ function deepMerge(target:any,other:any){ const targetToString=Object.prototype.toString.call(target); const otherToString=Object.prototype.toString.call(target); if(targetToString==="[object Object]" && otherToString==="[object Object]"){ for(let [key,val] of Object.entries(other)){ if(!target[key]){ target[key]=val; }else{ target[key]=deepMerge(target[key],val); } } }else if(targetToString==="[object Array]" && otherToString==="[object Array]"){ for(let [key,val] of Object.entries(other)){ if(target[key]){ target[key]=deepMerge(target[key],val); }else{ target.push(val); } } } return target; }
總結
程式碼的主要問題是判斷型別,使用typeof
是萬萬不行的,你會發現對 null、陣列、物件使用,得到的結果是一致的:
例子 | 使用typeof 得到的字串 |
---|---|
null | "object" |
[] | "object" |
{} | "object" |
function(){} | "function" |
1 | "number" |
"" | "string" |
true | "boolean" |
undefined | "undefined" |
Symbol(1) | "symbol" |
instanceof
也是不行,因為它會從原型鏈上尋找,從而導致很多時候得到的結果不符合人意。
完美的方案就是Object.prototype.toString.call()
:
例子 | 使用Object.prototype.toString.call() 得到的字串 |
---|---|
null | "[object Null]" |
[] | "[object Array]" |
{} | "[object Object]" |
function(){} | "[object Function]" |
1 | "[object Number]" |
"" | "[object String]" |
true | "[object Boolean]" |
undefined | "[object Undefined]" |
Symbol(1) | "[object Symbol]" |
你可以能會好奇,變數自帶的toString()
可以使用嗎?答案是不可以的,在ECMAObject.prototype.toString()
中的解釋如下:
Object.prototype.toString()
When the toString method is called, the following steps are taken:
- Get the [[Class]] property of this object.
- Compute a string value by concatenating the three strings “[object “, Result (1), and “]”.
- Return Result (2)
也就是說,Object.prototype.toString()
呼叫時,它會對呼叫者本身的[[Class]]
[1]屬性,再拼接成"[object " + [[Class]] + "]"
的形式返回。
但是toString()
是可以覆寫的,每個常見的 Object 子類都進行了相應的改寫,比如陣列呼叫時:
[].toString();
// ''
// 一個空字串
(function(){}).toString();
// 'function(){}'
1..toString();
// '1'
// 陣列是基本型別,這裡會把數字包裝成 Number 型別再進行呼叫,而 Number 就是物件
注意第三個例子, 1 後面是兩個點,原因在這:唯一數字型別:number
-
在 JS 中,
[[*]]
這種以雙括號包裹的格式的屬性,你無法用程式碼直接訪問,因為這是給 JS 引擎使用的。我猜測應該來自 java 語言,因為 JVM 每載入一個類,都會生成對應的Class
類。這個Class
會記載載入的類的相關資訊,比如這個類的函式、函式引數、屬性等等。 ↩︎