对象属性深度合并与覆盖方法

需求

实现一个通用方法,要求

  • 实现类似Object.assign()的属性覆盖功能,且当对象内部有重名子对象时,对重名的子对象也实现属性覆盖与合并

Object.assign()

Object.assign()实现浅拷贝
对于内部子对象时传递的引用

Object.assign()的定义与声明

Object.assign()源码

if (typeof Object.assign !== 'function') {
    // Must be writable: true, enumerable: false, configurable: true
    Object.defineProperty(Object, "assign", {
        value: function assign(target, varArgs) { // .length of function is 2
            'use strict';
            if (target === null || target === undefined) {
                throw new TypeError('Cannot convert undefined or null to object');
                //不允许向空目标中合并
            }
            var to = Object(target);
            //arguments 参数表,arguments[0]是target
            for (var index = 1; index < arguments.length; index++) {
                var nextSource = arguments[index];

                if (nextSource !== null && nextSource !== undefined) {
                    for (var nextKey in nextSource) {
                        // Avoid bugs when hasOwnProperty is shadowed
                        if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                            to[nextKey] = nextSource[nextKey];
                        }
                    }
                }
            }
            return to;
        },
        writable: true,
        configurable: true
    });
}

用例1

const obj1 = {
    a: 1,
    b: 2,
}
const obj2 = {
    a: 3,
    c: 4,
}
const obj3 = Object.assign(obj1, obj2);
console.log(obj3)
//{ a: 3, b: 2, c: 4 }

用例2

const a = {
    c: 2
}
const b = {
    d: 3
}
const res = Object.assign(a, b);
console.log(res);
//{ c: 2, d: 3 }
a.c = 4;
b.d = 5;
console.log(res);
//{ c: 4, d: 3 }

可以初步察觉,Object.assign(a,b)返回的是a的浅拷贝

用例3

const obj1 = {
    a: 1,
    b: {
        c: 2,
        d: 4,
    },
}
const obj2 = {
    a: 3,
    b: {
        c: 3,
        e: 5,
    },
}
const obj3 = Object.assign(obj1, obj2);
console.log(obj3)
//{ a: 3, b: { c: 3, e: 5 } }

我希望在用例2中,合并obj1与obj2时可以得到{ a: 3, b: { c: 3, d:4,e: 5 } },即子对象也应用Object.assign()

实现

思路

​ 使用与Object.assign类似的思路,将浅拷贝换成深拷贝,并对子对象进行递归拷贝覆盖

  1. 执行函数assign(target,orign),orign为覆盖的数据,target为被覆盖的数据

  2. 检查target和orign的类型

    1. 如果有一个是null类型,则返回orign的深拷贝

    2. 如果有一个是普通基础类型,则返回orign的深拷贝,否则进入下一步小步

      1. 如果有一个是array,返回orign的深拷贝

      2. 深拷贝target,存入到resp中

      3. 开始遍历orign中的key

        1. 如果resp也存在key,则进行递归,跳转到第1步,参数target=resp[key],orign=orign[key]
        2. 如果resp中不存在key属性,则直接合并
      4. 结束遍历返回resp

测试

用例1

const obj1 = {
    a: 1,
    b: {
        c: 2,
        d: 4,
    },
}
const obj2 = {
    a: 3,
    b: {
        c: 3,
        e: 5,
    },
}
const res1 = assign(obj1, obj2);
console.log(res1);//{ a: 3, b: { c: 3, d: 4, e: 5 } }
obj1.a = 4;
obj1.b.c = 5;
console.log(res1);//{ a: 3, b: { c: 3, d: 4, e: 5 } }

输出

{ a: 3, b: { c: 3, d: 4, e: 5 } }
{ a: 3, b: { c: 3, d: 4, e: 5 } }

用例2

const obj5 = {
    a: 2,
    b: {
        f: 2,
        g: {
            h: [4, 5],
            i: {
                a: 123
            }
        },
        x: 13
    },
    c: [1, 2, 3],
    d: false,
    f: 5,
    g: { a: 1 },
    h: null,
    i: { a: 3 },
    k: {}
}
const obj6 = {
    a: {
        e: 3
    },
    b: {
        f: 20,
        g: {
            h: [41, 5],
            i: {
                a: 1234
            }
        },
        y: 14
    },
    c: [2],
    d: true,
    f: [1, 23],
    g: [2, 3],
    h: { a: 3 },
    i: null,
    k: { a: 1 },
    l: [4, 5]
}
const res3 = assign(obj5, obj6);
console.log(JSON.stringify(res3));
//{"a":{"e":3},"b":{"f":20,"g":{"h":[41,5],"i":{"a":1234}},"x":13,"y":14},"c":[2],"d":true,"f":[1,23],"g":[2,3],"h":{"a":3},"i":null,"k":{"a":1},"l":[4,5]}
obj5.c.push(1)
obj6.c.push(3)
obj6.f.push(4)
obj6.l.push(8)
console.log(JSON.stringify(res3));
//{"a":{"e":3},"b":{"f":20,"g":{"h":[41,5],"i":{"a":1234}},"x":13,"y":14},"c":[2],"d":true,"f":[1,23],"g":[2,3],"h":{"a":3},"i":null,"k":{"a":1},"l":[4,5]}

输出

{"a":{"e":3},"b":{"f":20,"g":{"h":[41,5],"i":{"a":1234}},"x":13,"y":14},"c":[2],"d":true,"f":[1,23],"g":[2,3],"h":{"a":3},"i":null,"k":{"a":1},"l":[4,5]}
{"a":{"e":3},"b":{"f":20,"g":{"h":[41,5],"i":{"a":1234}},"x":13,"y":14},"c":[2],"d":true,"f":[1,23],"g":[2,3],"h":{"a":3},"i":null,"k":{"a":1},"l":[4,5]}

可以看见,在上述的两个测试用例中,assign()函数都很好的达成了我需要的功能,并且返回的也是一个新对象,避免了返回对象引用带来的问题。

当然,由于频繁的进行深拷贝,和普通的Object.assign()相比,我封装的assign显然更加耗能。

源码

/**
 * 
 * @param target 目标对象 被覆盖的对象
 * @param orign  源对象 覆盖的对象
 * @returns 
 */
function assign(target: any, orign: any) {
    if (orign === null || target === null) return JSON.parse(JSON.stringify(orign))
    if (typeof target === 'object' && typeof orign === 'object') {
        if (Array.isArray(orign) || Array.isArray(target)) {
            return JSON.parse(JSON.stringify(orign))
        }
        //深拷贝target
        let resp = JSON.parse(JSON.stringify(target));
        for (let key in orign) {
            if (key in resp) {
                resp[key] = assign(resp[key], orign[key]);
            } else {
                //如果resp中不存在key属性,则直接合并
                resp[key] = JSON.parse(JSON.stringify(orign[key]))
            }
        }
        return resp
    } else {
        //如果有一个是基础类型,则直接返回第二个参数替换
        return JSON.parse(JSON.stringify(orign))
    }
}

进阶

前面实现了将一个对象合并到另一个对象上的方法,现在来实现将多个对象合并到同一对象上的方法

思路

同样类似于Object.assign(),使用参数表来进行一个遍历合并的操作

测试

const obj1 = {
    a: 1,
    b: {
        c: 2,
        d: 4,
    },
}
const obj2 = {
    a: 2,
    b: {
        c: 3,
        e: 5,
        f: 6,
    },
}
const obj3 = null
const obj4 = {
    a: 3,
    b: {
        c: 4,
        d: 5,
        f: 7,
    }
}
const resp = assign(obj1, obj2, obj3, obj4)
console.log(resp)//{ a: 3, b: { c: 4, d: 5, e: 5, f: 7 } }

输出

{ a: 3, b: { c: 4, d: 5, e: 5, f: 7 } }

源码

/**
 *
 * @param target 目标对象 被覆盖的对象
 * @param orign  源对象 覆盖的对象
 * @returns
 */
function func(target: any, orign: any) {
    if (orign === null || target === null) return JSON.parse(JSON.stringify(orign))
    if (typeof target === 'object' && typeof orign === 'object') {
        if (Array.isArray(orign) || Array.isArray(target)) {
            return JSON.parse(JSON.stringify(orign))
        }
        //深拷贝target
        let resp = JSON.parse(JSON.stringify(target));
        for (let key in orign) {
            if (key in resp) {
                resp[key] = func(resp[key], orign[key]);
            } else {
                //如果resp中不存在key属性,则直接合并
                resp[key] = JSON.parse(JSON.stringify(orign[key]))
            }
        }
        return resp
    } else {
        //如果有一个是基础类型,则直接返回第二个参数替换
        return JSON.parse(JSON.stringify(orign))
    }
}
/**
 *
 * @param target 目标对象 被覆盖的对象
 * @param others  源对象 覆盖的对象
 * @returns
 */
function assign(target: any, ...others: any) {
    if (target === null || target === undefined) {
        throw new TypeError('Cannot convert undefined or null to object');
        //不允许向空目标中合并
    }
    let resp = JSON.parse(JSON.stringify(target))
    for (let index = 1; index < arguments.length; index++) {
        let nextSource = arguments[index];
        if (nextSource !== null && nextSource !== undefined) {
            resp = func(resp, nextSource)
        }
    }
    return resp
}