1. 程式人生 > 其它 >js 深度合併兩個物件

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:

  1. Get the [[Class]] property of this object.
  2. Compute a string value by concatenating the three strings “[object “, Result (1), and “]”.
  3. Return Result (2)

也就是說,Object.prototype.toString()呼叫時,它會對呼叫者本身的[[Class]][1]屬性,再拼接成"[object " + [[Class]] + "]"的形式返回。
但是toString()是可以覆寫的,每個常見的 Object 子類都進行了相應的改寫,比如陣列呼叫時:

[].toString();
// ''
// 一個空字串

(function(){}).toString();
// 'function(){}'

1..toString();
// '1'
// 陣列是基本型別,這裡會把數字包裝成 Number 型別再進行呼叫,而 Number 就是物件

注意第三個例子, 1 後面是兩個點,原因在這:唯一數字型別:number


  1. 在 JS 中,[[*]]這種以雙括號包裹的格式的屬性,你無法用程式碼直接訪問,因為這是給 JS 引擎使用的。我猜測應該來自 java 語言,因為 JVM 每載入一個類,都會生成對應的Class類。這個Class會記載載入的類的相關資訊,比如這個類的函式、函式引數、屬性等等。 ↩︎