起因
今天使用 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:
- 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 就是對象
所以變量自帶的toString()
是不可以調用的,不然得到的結果會和上述代碼一樣,奇奇怪怪!
注意第三個例子, 1 后面是兩個點,原因在這:唯一數字類型:number
在 JS 中,
[[*]]
這種以雙括號包裹的格式的屬性,你無法用代碼直接訪問,因為這是給 JS 引擎使用的。我猜測應該來自 java 語言,因為 JVM 每加載一個類,都會生成對應的Class
類。這個Class
會記載加載的類的相關信息,比如這個類的函數、函數參數、屬性等等。 ↩︎