如何深度合并而不是浅层合并?
Object.assign和Object spread只做一个浅层合并。
问题的一个例子:
// No object nesting const x = { a: 1 } const y = { b: 1 } const z = { ...x, ...y } // { a: 1, b: 1 }
输出是你所期望的。 但是,如果我尝试这个:
// Object nesting const x = { a: { a: 1 } } const y = { a: { b: 1 } } const z = { ...x, ...y } // { a: { b: 1 } }
代替
{ a: { a: 1, b: 1 } }
你得到
{ a: { b: 1 } }
x被完全覆盖,因为扩展运算符只有一个深度。 这与Object.assign()
是一样的。
有没有办法做到这一点?
有谁知道在ES6 / ES7规范中是否存在深度合并?
不,不是的。
我知道这是一个老问题,但ES2015 / ES6中最简单的解决scheme实际上很简单,使用Object.assign(),
希望这有助于:
/** * Simple object check. * @param item * @returns {boolean} */ export function isObject(item) { return (item && typeof item === 'object' && !Array.isArray(item)); } /** * Deep merge two objects. * @param target * @param ...sources */ export function mergeDeep(target, ...sources) { if (!sources.length) return target; const source = sources.shift(); if (isObject(target) && isObject(source)) { for (const key in source) { if (isObject(source[key])) { if (!target[key]) Object.assign(target, { [key]: {} }); mergeDeep(target[key], source[key]); } else { Object.assign(target, { [key]: source[key] }); } } } return mergeDeep(target, ...sources); }
用法示例:
mergeDeep(this, { a: { b: { c: 123 } } }); // or const merged = mergeDeep({a: 1}, { b : { c: { d: { e: 12345}}}}); console.dir(merged); // { a: 1, b: { c: { d: [Object] } } }
你会在下面的答案中find这个不可变的版本。
请注意,这将导致循环引用的无限recursion。 如果你认为你会遇到这个问题,那么在这里有关于如何检测循环引用的一些很好的答案。
当涉及到主机对象或任何types的比一包值更复杂的对象时,这个问题并不是微不足道的
- 你调用一个getter来获得一个值,或者你复制的属性描述符?
- 如果合并目标有一个setter(自己的财产或在其原型链)? 你认为这个值是否已经存在,或者调用setter来更新当前值?
- 你调用自己的财产function或复制它们? 如果他们是绑定函数或箭头函数,取决于在他们定义的时间范围链中的某些东西呢?
- 如果它像一个DOM节点呢? 你当然不希望把它看作简单的对象,只是把它所有的属性深入合并
- 如何处理像数组,地图或集合这样的“简单”结构? 考虑他们已经存在或合并呢?
- 如何处理不可枚举的自己的属性?
- 那么新的子树呢? 只需通过引用或深度克隆进行分配?
- 如何处理冻结/密封/不可扩展的对象?
还有一件事要记住:包含循环的对象图。 通常不难处理 – 只保留一Set
已经访问过的源对象 – 但经常被遗忘。
您可能应该编写一个深度合并函数,该函数仅将原始值和简单对象(至多是结构化克隆algorithm可以处理的那些types)视为合并源。 抛出如果它遇到任何它无法处理的东西,或者只是通过引用而不是深层合并来分配。
换句话说,没有一种万能的algorithm,你必须自己推出或者寻找一个恰好覆盖你的用例的库方法。
这是@ Salakar答案的不可变(不修改input)版本。 有用的,如果你正在做function编程types的东西。
export function isObject(item) { return (item && typeof item === 'object' && !Array.isArray(item)); } export default function mergeDeep(target, source) { let output = Object.assign({}, target); if (isObject(target) && isObject(source)) { Object.keys(source).forEach(key => { if (isObject(source[key])) { if (!(key in target)) Object.assign(output, { [key]: source[key] }); else output[key] = mergeDeep(target[key], source[key]); } else { Object.assign(output, { [key]: source[key] }); } }); } return output; }
您可以使用Lodash合并 :
var object = { 'a': [{ 'b': 2 }, { 'd': 4 }] }; var other = { 'a': [{ 'c': 3 }, { 'e': 5 }] }; _.merge(object, other); // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }
如果你正在使用ImmutableJS,你可以使用mergeDeep
:
fromJS(options).mergeDeep(options2).toJS();
它不存在,但你可以使用JSON.parse(JSON.stringify(jobs))
这是我刚刚写的支持数组的另一个。 它连接他们。
function isObject(obj) { return obj !== null && typeof obj === 'object'; } function isPlainObject(obj) { return isObject(obj) && ( obj.constructor === Object // obj = {} || obj.constructor === undefined // obj = Object.create(null) ); } function mergeDeep(target, ...sources) { if (!sources.length) return target; const source = sources.shift(); if(Array.isArray(target)) { if(Array.isArray(source)) { target.push(...source); } else { target.push(source); } } else if(isPlainObject(target)) { if(isPlainObject(source)) { for(let key of Object.keys(source)) { if(!target[key]) { target[key] = source[key]; } else { mergeDeep(target[key], source[key]); } } } else { throw new Error(`Cannot merge object with non-object`); } } else { target = source; } return mergeDeep(target, ...sources); };
我们可以使用$ .extend(true,object1,object2)进行深度合并。 值true表示recursion合并两个对象,修改第一个。
$扩展(真,目标,对象)
有谁知道在ES6 / ES7规范中是否存在深度合并?
Object.assign文档build议它不做深层克隆。
有时候,即使你这样认为,也不需要深度合并。 例如,如果你有一个嵌套对象的默认configuration,并且你想用你自己的configuration深入地扩展它,你可以为它创build一个类。 这个概念很简单:
function AjaxConfig(config) { // Default values + config Object.assign(this, { method: 'POST', contentType: 'text/plain' }, config); // Default values in nested objects this.headers = Object.assign({}, this.headers, { 'X-Requested-With': 'custom' }); } // Define your config var config = { url: 'https://google.com', headers: { 'x-client-data': 'CI22yQEI' } }; // Extend the default values with your own var fullMergedConfig = new AjaxConfig(config); // View in DevTools console.log(fullMergedConfig);
你可以把它转换成一个函数(不是构造函数)。
function isObject(obj) { return obj !== null && typeof obj === 'object'; } const isArray = Array.isArray; function isPlainObject(obj) { return isObject(obj) && ( obj.constructor === Object // obj = {} || obj.constructor === undefined // obj = Object.create(null) ); } function mergeDeep(target, ...sources){ if (!sources.length) return target; const source = sources.shift(); if (isPlainObject(source) || isArray(source)) { for (const key in source) { if (isPlainObject(source[key]) || isArray(source[key])) { if (isPlainObject(source[key]) && !isPlainObject(target[key])) { target[key] = {}; }else if (isArray(source[key]) && !isArray(target[key])) { target[key] = []; } mergeDeep(target[key], source[key]); } else if (source[key] !== undefined && source[key] !== '') { target[key] = source[key]; } } } return mergeDeep(target, ...sources); } // test... var source = {b:333}; var source2 = {c:32, arr: [33,11]} var n = mergeDeep({a:33}, source, source2); source2.arr[1] = 22; console.log(n.arr); // out: [33, 11]
这里是TypeScript的实现:
export const mergeObjects = <T extends object = object>(target: T, ...sources: T[]): T => { if (!sources.length) { return target; } const source = sources.shift(); if (source === undefined) { return target; } if (isMergebleObject(target) && isMergebleObject(source)) { Object.keys(source).forEach(function(key: string) { if (isMergebleObject(source[key])) { if (!target[key]) { target[key] = {}; } mergeObjects(target[key], source[key]); } else { target[key] = source[key]; } }); } return mergeObjects(target, ...sources); }; const isObject = (item: any): boolean => { return item !== null && typeof item === 'object'; }; const isMergebleObject = (item): boolean => { return isObject(item) && !Array.isArray(item); };
和unit testing:
describe('merge', () => { it('should merge Objects and all nested Ones', () => { const obj1 = { a: { a1: 'A1'}, c: 'C', d: {} }; const obj2 = { a: { a2: 'A2'}, b: { b1: 'B1'}, d: null }; const obj3 = { a: { a1: 'A1', a2: 'A2'}, b: { b1: 'B1'}, c: 'C', d: null}; expect(mergeObjects({}, obj1, obj2)).toEqual(obj3); }); it('should behave like Object.assign on the top level', () => { const obj1 = { a: { a1: 'A1'}, c: 'C'}; const obj2 = { a: undefined, b: { b1: 'B1'}}; expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2)); }); it('should not merge array values, just override', () => { const obj1 = {a: ['A', 'B']}; const obj2 = {a: ['C'], b: ['D']}; expect(mergeObjects({}, obj1, obj2)).toEqual({a: ['C'], b: ['D']}); }); it('typed merge', () => { expect(mergeObjects<TestPosition>(new TestPosition(0, 0), new TestPosition(1, 1))) .toEqual(new TestPosition(1, 1)); }); }); class TestPosition { constructor(public x: number = 0, public y: number = 0) {/*empty*/} }
我使用es6进行深度分配。
function isObject(item) { return (item && typeof item === 'object' && !Array.isArray(item) && item !== null) } function deepAssign(...objs) { if (objs.length < 2) { throw new Error('Need two or more objects to merge') } const target = objs[0] for (let i = 1; i < objs.length; i++) { const source = objs[i] Object.keys(source).forEach(prop => { const value = source[prop] if (isObject(value)) { if (target.hasOwnProperty(prop) && isObject(target[prop])) { target[prop] = deepAssign(target[prop], value) } else { target[prop] = value } } else if (Array.isArray(value)) { if (target.hasOwnProperty(prop) && Array.isArray(target[prop])) { const targetArray = target[prop] value.forEach((sourceItem, itemIndex) => { if (itemIndex < targetArray.length) { const targetItem = targetArray[itemIndex] if (Object.is(targetItem, sourceItem)) { return } if (isObject(targetItem) && isObject(sourceItem)) { targetArray[itemIndex] = deepAssign(targetItem, sourceItem) } else if (Array.isArray(targetItem) && Array.isArray(sourceItem)) { targetArray[itemIndex] = deepAssign(targetItem, sourceItem) } else { targetArray[itemIndex] = sourceItem } } else { targetArray.push(sourceItem) } }) } else { target[prop] = value } } else { target[prop] = value } }) } return target }
我试图写一个Object.assignDeep
,它是基于mdn上的Object.assign
的pollyfill 。
(ES5)
Object.assignDeep = function (target, varArgs) { // .length of function is 2 'use strict'; if (target == null) { // TypeError if undefined or null throw new TypeError('Cannot convert undefined or null to object'); } var to = Object(target); for (var index = 1; index < arguments.length; index++) { var nextSource = arguments[index]; if (nextSource != null) { // Skip over if undefined or null for (var nextKey in nextSource) { // Avoid bugs when hasOwnProperty is shadowed if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { if (typeof to[nextKey] === 'object' && to[nextKey] && typeof nextSource[nextKey] === 'object' && nextSource[nextKey]) { Object.assignDeep(to[nextKey], nextSource[nextKey]); } else { to[nextKey] = nextSource[nextKey]; } } } } } return to; }; console.log(Object.assignDeep({},{a:{b:{c:1,d:1}}},{a:{b:{c:2,e:2}}}))
如果你想避免创build一个特定的方法,但仍然需要改变一个对象的深度值而不改变它,你可以这样做:
let obj = {a: {b: {c: false, other:''}, other: ''}, other: ''}; // making obj.abc = true let newObj = Object.assign(obj, { a: Object.assign(obj.a, { b: Object.assign(obj.ab, {c: true}) }) });
不太可读,但有效。
这很简单,工作:
let item = { firstName: 'Jonnie', lastName: 'Walker', fullName: function fullName() { return 'Jonnie Walker'; } Object.assign(Object.create(item), item);
说明:
Object.create()
创build新的对象。 如果你传递params函数它会创build你的对象与其他对象的原型。 所以如果你在对象的原型上有任何的function,它们将被传递给其他对象的原型。
Object.assign()
合并两个对象并创build全新的对象,并且不再有任何引用。 所以这个例子对我很好。