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()可以使用嗎?答案是不可以的,在 ECMA 中 Object.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 就是對象

所以變量自帶的toString()是不可以調用的,不然得到的結果會和上述代碼一樣,奇奇怪怪!
注意第三個例子, 1 后面是兩個點,原因在這:唯一數字類型:number


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


免責聲明!

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



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