Git Merge合并的那些坑我帮你踩过了
merge搞不定,数据覆盖把我整懵了
今天遇到个蛋疼的问题,本来以为很简单的一个对象合并操作,结果被坑得够呛。项目里有个需求是动态更新用户配置,每次收到新数据的时候要把老配置和新配置合并,结果写出来的代码每次都是新数据完全替换掉老数据,根本不是我想要的深度合并效果。
折腾了半天发现是 merge 方法没用对,或者说用错了工具。刚开始直接用 Object.assign(),看起来能跑,实际上深层嵌套的数据一合并就炸了。
Object.assign() 踩的第一个坑
先说说我最初的写法:
const oldConfig = {
user: {
name: '张三',
profile: {
age: 25,
hobby: ['读书', '游戏']
}
},
settings: {
theme: 'dark'
}
};
const newConfig = {
user: {
profile: {
age: 26
}
}
};
// 错误的做法
const merged = Object.assign({}, oldConfig, newConfig);
console.log(merged);
// 结果 user.name 直接没了,整个 user 对象都被 newConfig.user 替换了
这里我踩了个大坑,Object.assign() 是浅拷贝,遇到嵌套对象的时候就会把整个对象替换成新的,而不是把里面的属性合并。所以 user 下面的所有属性都没了,只剩下一个 profile。
lodash.merge 确实靠谱,但项目不能引入库
后面我试了 lodash 的 merge 方法,确实好用:
import { merge } from 'lodash';
const oldConfig = {
user: {
name: '张三',
profile: {
age: 25,
hobby: ['读书', '游戏']
}
}
};
const newConfig = {
user: {
profile: {
age: 26,
city: '北京'
}
}
};
const result = merge({}, oldConfig, newConfig);
console.log(result);
// user.name 保留了,profile.age 更新了,还加了 city 字段
但是客户那边比较严格,不让引入第三方库,所以这个方案 pass 掉了。
自己手写 deepMerge,边界情况真多
没办法只能自己手写了,网上找了个基础版本改了改,结果发现各种边界情况要考虑:
function deepMerge(target, source) {
const output = { ...target };
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => {
if (isObject(source[key])) {
if (!(key in target)) {
output[key] = source[key];
} else {
output[key] = deepMerge(target[key], source[key]);
}
} else {
output[key] = source[key];
}
});
}
return output;
}
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
写完之后测试了一下,发现数组处理有问题。比如用户的 hobby 数组,我希望的是替换而不是合并,但上面的逻辑会把数组当普通对象来处理。后来改成这样:
function deepMerge(target, source) {
const output = Array.isArray(target) ? [...target] : { ...target };
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => {
if (isObject(source[key])) {
if (!(key in target)) {
// 如果目标不存在该属性,直接赋值
output[key] = source[key];
} else {
// 如果都存在,递归合并
output[key] = deepMerge(target[key], source[key]);
}
} else if (Array.isArray(source[key])) {
// 数组直接替换,不合并
output[key] = [...source[key]];
} else {
// 基本类型直接覆盖
output[key] = source[key];
}
});
}
return output;
}
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
ES6 扩展运算符也不是万能的
一开始我还想着用扩展运算符一行搞定:
const merged = {...oldConfig, ...newConfig};
结果一样是浅合并的问题,深层对象还是会被整个替换掉。不过对于一层的对象确实挺方便的,就是不能处理复杂嵌套。
最终方案和注意事项
最后我用了改进版的 deepMerge 函数,但发现还有几个要注意的地方:
- 循环引用要处理,不然会栈溢出
- null 和 undefined 要特别判断
- Date 对象要不要特殊处理
- Symbol 类型也要考虑
这是最终用的版本:
function deepMerge(target, source) {
// 防止循环引用
if (target === source || source == null) return target;
const output = Array.isArray(target) ? [...target] : { ...target };
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => {
if (isObject(source[key]) && target[key]) {
output[key] = deepMerge(target[key], source[key]);
} else if (Array.isArray(source[key])) {
output[key] = [...source[key]];
} else if (source[key] !== undefined && source[key] !== null) {
output[key] = source[key];
}
});
} else if (source) {
return source;
}
return output;
}
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Date);
}
// 测试一下
const config1 = {
user: {
name: '张三',
profile: {
age: 25,
hobby: ['读书', '游戏'],
address: {
home: '北京',
work: '朝阳'
}
}
},
settings: {
theme: 'light'
}
};
const config2 = {
user: {
profile: {
age: 26,
city: '上海',
address: {
home: '上海'
}
}
},
settings: {
language: 'zh-CN'
}
};
const result = deepMerge(config1, config2);
console.log(result);
// 现在能正常合并了,name 保留,age 更新,新增 city,home 地址也更新了
到这里基本解决了问题,不过还是有点瑕疵。比如如果新数据传过来某些字段是 null,现在是会覆盖掉原来的值,可能需要根据业务需求调整 null 值的处理逻辑。但总体来说能用,暂时先这么处理吧。
性能考虑和后续优化
后面我还注意到一个问题,就是每次合并都会创建新对象,频繁操作的话可能会有性能影响。但对于我这个项目的使用场景,配置更新频率不高,应该问题不大。如果后期发现性能瓶颈,可能要考虑用 immutable.js 或者类似的不可变数据结构库了。
还有一个小问题就是错误处理,现在的代码没有做太多异常处理,万一传入的数据格式不对可能会报错。不过考虑到数据来源可控,暂时先这样了。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

暂无评论