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会记载加载的类的相关信息,比如这个类的函数、函数参数、属性等等。 ↩︎

posted @ 2022-03-23 09:40  Sebastian·S·Pan  阅读(1240)  评论(0编辑  收藏  举报