Как глубокое слияние вместо мелкого слияния?
и
26 ответов:
Я знаю, что это немного старая проблема, но самое простое решение в ES2015/ES6, которое я мог бы придумать, было на самом деле довольно простым, используя объект.назначить (),
надеюсь, это поможет:
/** * 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] } } }
вы найдете неизменяемую версию этого в ответе ниже.
обратите внимание, что это приведет к бесконечной рекурсии на циклические ссылки. здесь есть несколько отличных ответов о том, как обнаружить круговой ссылки, если вы думаете, что столкнетесь с этой проблемой.
проблема нетривиальна, когда дело доходит до размещения объектов или любого объекта, который сложнее, чем мешок значений
- вы вызываете геттер для получения значения или копируете над дескриптором свойства?
- что делать, если цель слияния имеет сеттер (либо собственное свойство, либо в его цепочке прототипов)? Считаете ли вы значение уже существующим или вызываете сеттер для обновления текущего значения?
- вызываете ли вы функции собственного свойства или копируете они закончились? Что делать, если они являются связанными функциями или функциями стрелки в зависимости от чего-то в их цепочке областей на момент их определения?
- что если это что-то вроде узла DOM? Вы, конечно, не хотите рассматривать его как простой объект и просто глубоко объединить все его свойства в
- как иметь дело с "простыми" структурами, такими как массивы или карты или наборы? Считайте их уже существующими или объедините их тоже?
- как бороться с неисчислимыми собственными недвижимость?
- как насчет новых поддеревьев? Просто назначить по ссылке или глубокий клон?
- как иметь дело с замороженными / запечатанными / нерасширяемыми объектами?
еще одна вещь, чтобы иметь в виду: графы объектов, которые содержат циклы. Это, как правило, не трудно иметь дело - просто держать
Set
уже посещенных исходных объектов-но часто забытых.вероятно, вам следует написать функцию глубокого слияния, которая ожидает только примитивные значения и простые объекты - в большинстве тех типов, что структурированный алгоритм клонирования может обрабатывать - как слить источников. Бросьте, если он сталкивается с чем-то, что он не может обработать или просто назначить по ссылке вместо глубокого слияния.
другими словами, нет единого алгоритма для всех, вам либо нужно свернуть свой собственный, либо искать метод библиотеки, который будет охватывать ваши варианты использования.
можно использовать Лодашь слияния:
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 }] }
вот неизменяемая (не изменяет входные данные) версия ответа @Salakar. Полезно, если вы делаете функциональное программирование типа вещи.
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; }
я знаю, что есть много ответов уже и как много комментариев, утверждающих, что они не будут работать. Единственный консенсус заключается в том, что это так сложно, что никто не сделал стандартный для него. Тем не менее, большинство принятых ответов в так разоблачают "простые трюки", которые широко используются. Итак, для всех нас, таких как я, которые не являются экспертами, но хотят писать более безопасный код, понимая немного больше о сложности javascript, я попытаюсь пролить свет.
прежде чем получить наш руки грязные, позвольте уточнить 2 момента:
- [отказ от ответственности] я предлагаю функцию ниже, которая решает, как мы глубокий цикл на объекты javascript для копирования и иллюстрирует то, что обычно слишком коротко комментируется. Он не готов к производству. Для ясности я намеренно оставил в стороне другие соображения, такие как круговые объекты (трек по набору или неконфликтному свойству символа) копирование эталонного значения или глубокий клон, неизменяемый объект назначения (глубокий клон снова?), в каждом конкретном случае исследование каждый тип объектов, получить/установить свойства через аксессоры... Кроме того, я не тестировал производительность, хотя это важно, потому что это тоже не имеет значения.
- я буду использовать скопировать или присвоить термины вместо слияние. Потому что в моем сознании a слияние консервативен и должен потерпеть неудачу при конфликтах. Здесь, когда конфликтует, мы хотим, чтобы источник перезаписал назначение. Как
Object.assign
делает.ответы
for..in
илиObject.keys
заблуждениесоздание глубокой копии кажется настолько простой и распространенной практикой, что мы ожидаем найти однострочный или, по крайней мере, быстрый выигрыш с помощью простой рекурсии. Мы не ожидаем, что нам понадобится библиотека или написать пользовательскую функцию на 100 строк.
когда я впервые прочитала Salakar это, я искренне думал, что я мог сделать лучше и проще (вы можете сравнить его с
Object.assign
onx={a:1}, y={a:{b:1}}
). Потом я прочитал ответ the8472 и я думал... так легко не уйти, улучшение уже данных ответов не приведет нас далеко.давайте глубокая копия и рекурсивная в сторону мгновения. Просто подумайте, как (ошибочно) люди анализируют свойства, чтобы скопировать очень простой объект.
const y = Object.create( { proto : 1 }, { a: { enumerable: true, value: 1}, [Symbol('b')] : { enumerable: true, value: 1} } ) Object.assign({},y) > { 'a': 1, Symbol(b): 1 } // All (enumerable) properties are copied ((x,y) => Object.keys(y).reduce((acc,k) => Object.assign(acc, { [k]: y[k] }), x))({},y) > { 'a': 1 } // Missing a property! ((x,y) => {for (let k in y) x[k]=y[k];return x})({},y) > { 'a': 1, 'proto': 1 } // Missing a property! Prototype's property is copied too!
Object.keys
будут опущены собственные не перечислимые свойства, собственные символьные свойства и все свойства прототипа. Это может быть хорошо, если ваши объекты не имеют ни одного из них. Но имейте в виду, чтоObject.assign
обрабатывает собственные перечислимые свойства с символьной клавишей. Таким образом, ваша пользовательская копия потеряла свой цвет.
for..in
обеспечит свойства источника, его прототипа и полной цепи прототипов без вашего желания (или зная его). Ваша цель может иметь слишком много свойств, смешивая свойства прототипа и собственные свойства.если вы пишете функции общего назначения, и вы не используете
Object.getOwnPropertyDescriptors
,Object.getOwnPropertyNames
,Object.getOwnPropertySymbols
илиObject.getPrototypeOf
, вы, скорее всего, делаете это неправильно.вещи, чтобы рассмотреть, прежде чем писать свою функцию
во-первых, убедитесь, что вы понимаете, что такое объект Javascript. В Javascript объект состоит из собственных свойств и (родительского) объекта-прототипа. Объект-прототип в свою очередь состоит из собственных свойств и объекта-прототипа. И так далее, определяя прототип цепь.
свойство-это пара ключ (
string
илиsymbol
) и описание (value
илиget
/set
accessor, и атрибуты, такие какenumerable
).наконец, есть многие типы объектов. Вы можете обрабатывать объект object по-разному от даты объекта или функции объекта.
Итак, написав свою глубокую копию, вы должны ответить хотя бы на эти вопросы:
- что я считаю глубоким (правильным для рекурсивного поиска вверх) или плоский?
- какие свойства я хочу скопировать? (перечислимые/не перечислимые, строковые/символьные, собственные свойства/собственные свойства прототипа, значения / дескрипторы...)
для моего примера, я считаю, что только
object Object
С глубокий, потому что другие объекты, созданные другими конструкторами, могут не подходить для углубленного просмотра. Подгонянный от это так.function toType(a) { // Get fine type (object, array, function, null, error, date ...) return ({}).toString.call(a).match(/([a-z]+)(:?\])/i)[1]; } function isDeepObject(obj) { return "Object" === toType(obj); }
и я сделал
options
объект выбрать, что копировать (для демонстрационных целей).const options = {nonEnum:true, symbols:true, descriptors: true, proto:true};
предлагаемая функция
вы можете проверить его в этот plunker.
function deepAssign(options) { return function deepAssignWithOptions (target, ...sources) { sources.forEach( (source) => { if (!isDeepObject(source) || !isDeepObject(target)) return; // Copy source's own properties into target's own properties function copyProperty(property) { const descriptor = Object.getOwnPropertyDescriptor(source, property); //default: omit non-enumerable properties if (descriptor.enumerable || options.nonEnum) { // Copy in-depth first if (isDeepObject(source[property]) && isDeepObject(target[property])) descriptor.value = deepAssign(options)(target[property], source[property]); //default: omit descriptors if (options.descriptors) Object.defineProperty(target, property, descriptor); // shallow copy descriptor else target[property] = descriptor.value; // shallow copy value only } } // Copy string-keyed properties Object.getOwnPropertyNames(source).forEach(copyProperty); //default: omit symbol-keyed properties if (options.symbols) Object.getOwnPropertySymbols(source).forEach(copyProperty); //default: omit prototype's own properties if (options.proto) // Copy souce prototype's own properties into target prototype's own properties deepAssign(Object.assign({},options,{proto:false})) (// Prevent deeper copy of the prototype chain Object.getPrototypeOf(target), Object.getPrototypeOf(source) ); }); return target; } }
это можно использовать так:
const x = { a: { a: 1 } }, y = { a: { b: 1 } }; deepAssign(options)(x,y); // { a: { a: 1, b: 1 } }
поскольку эта проблема все еще активна, вот еще один подход:
- ES6 / 2015
- неизменяемый (не изменяет исходные объекты)
- обрабатывает массивы (объединяет их)
/** * Performs a deep merge of objects and returns new object. Does not modify * objects (immutable) and merges arrays via concatenation. * * @param {...object} objects - Objects to merge * @returns {object} New object with merged key/values */ function mergeDeep(...objects) { const isObject = obj => obj && typeof obj === 'object'; return objects.reduce((prev, obj) => { Object.keys(obj).forEach(key => { const pVal = prev[key]; const oVal = obj[key]; if (Array.isArray(pVal) && Array.isArray(oVal)) { prev[key] = pVal.concat(...oVal); } else if (isObject(pVal) && isObject(oVal)) { prev[key] = mergeDeep(pVal, oVal); } else { prev[key] = oVal; } }); return prev; }, {}); } // Test objects const obj1 = { a: 1, b: 1, c: { x: 1, y: 1 }, d: [ 1, 1 ] } const obj2 = { b: 2, c: { y: 2, z: 2 }, d: [ 2, 2 ], e: 2 } const obj3 = mergeDeep(obj1, obj2); // Out console.log(obj3);
вот реализация 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); };
И Модульные Тесты:
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*/} }
Если вы используете ImmutableJS можно использовать
mergeDeep
:fromJS(options).mergeDeep(options2).toJS();
следующая функция делает глубокую копию объектов, она охватывает копирование примитива, массивы, а также объект
function mergeDeep (target, source) { if (typeof target == "object" && typeof source == "object") { for (const key in source) { if (source[key] === null && (target[key] === undefined || target[key] === null)) { target[key] = null; } else if (source[key] instanceof Array) { if (!target[key]) target[key] = []; //concatenate arrays target[key] = target[key].concat(source[key]); } else if (typeof source[key] == "object") { if (!target[key]) target[key] = {}; this.mergeDeep(target[key], source[key]); } else { target[key] = source[key]; } } } return target; }
у меня была эта проблема при загрузке кэшируется государственная тайна. Если я просто загружу кэшированное состояние, я столкнусь с ошибками для новой версии приложения с обновленной структурой состояния.
уже упоминалось, что лодаш предлагает свои
merge
функция, которую я использовал:const currentInitialState = configureState().getState(); const mergedState = _.merge({}, currentInitialState, cachedState); const store = configureState(mergedState);
простое решение с ES5 (перезаписать существующее значение):
function merge(current, update) { Object.keys(update).forEach(function(key) { // if update[key] exist, and it's not a string or array, // we go in one level deeper if (current.hasOwnProperty(key) && typeof current[key] === 'object' && !(current[key] instanceof Array)) { merge(current[key], update[key]); // if update[key] doesn't exist in current, or it's a string // or array, then assign/overwrite current[key] to update[key] } else { current[key] = update[key]; } }); return current; } var x = { a: { a: 1 } } var y = { a: { b: 1 } } console.log(merge(x, y));
вот еще одно решение ES6, работает с объектами и массивами.
function deepMerge(...sources) { let acc = {} for (const source of sources) { if (source instanceof Array) { if (!(acc instanceof Array)) { acc = [] } acc = [...acc, ...source] } else if (source instanceof Object) { for (let [key, value] of Object.entries(source)) { if (value instanceof Object && key in acc) { value = deepMerge(acc[key], value) } acc = { ...acc, [key]: value } } } } return acc }
можно использовать $.extend (true,object1,object2) для глубокого слияния. Значение true обозначает рекурсивное слияние двух объектов, изменяя первый.
вот еще один я только что написал, что поддерживает массивы. Это их объединяет.
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); };
я хотел бы представить довольно простую альтернативу ES5. Функция принимает 2 параметра -
target
иsource
это должно быть типа "объект".Target
будет результирующим объектом.Target
сохраняет все свои первоначальные свойства, но их значения могут быть изменены.function deepMerge(target, source) { if(typeof target !== 'object' || typeof source !== 'object') return false; // target or source or both ain't objects, merging doesn't make sense for(var prop in source) { if(!source.hasOwnProperty(prop)) continue; // take into consideration only object's own properties. if(prop in target) { // handling merging of two properties with equal names if(typeof target[prop] !== 'object') { target[prop] = source[prop]; } else { if(typeof source[prop] !== 'object') { target[prop] = source[prop]; } else { if(target[prop].concat && source[prop].concat) { // two arrays get concatenated target[prop] = target[prop].concat(source[prop]); } else { // two objects get merged recursively target[prop] = deepMerge(target[prop], source[prop]); } } } } else { // new properties get added to target target[prop] = source[prop]; } } return target; }
случаях:
- если
target
нетsource
собственностьtarget
получает ее;- если
target
естьsource
собственность иtarget
&source
не оба объекта (3 случая из 4),target
свойство переопределяется;- если
target
естьsource
свойство и оба они являются объектами / массивами (1 оставшийся случай), затем рекурсия происходит слияние двух объектов (или конкатенация двух массивов);также рассмотрим следующее:
- array + obj = array
- obj + array = obj
- obj + obj = obj (рекурсивно слились)
- array + array = array (concat)
он предсказуем, поддерживает примитивные типы, а также массивы и объекты. Также, как мы можем объединить 2 объекта, я думаю, что мы можем объединить более 2 через уменьшить
есть ли способ сделать это?
если библиотеки npm может быть использован в качестве решения, object-merge-advanced from Yours truly позволяет глубоко объединять объекты и настраивать/переопределять каждое действие слияния с помощью знакомой функции обратного вызова. Основная идея это больше, чем просто глубокое слияние - что происходит со значением, когда два ключа тот же? Эта библиотека заботится об этом-когда два ключа сталкиваются,
object-merge-advanced
весит типов, стремясь сохранить как можно больше данных после слияния:ключ первого входного аргумента отмечен #1, второго аргумента - #2. В зависимости от каждого типа, один выбирается для значения ключа результата. На диаграмме "объект" означает простой объект (не массив и т. д.).
когда ключи не сталкиваются, они все вводят результат.
из вашего примера фрагмента, если вы использовали
object-merge-advanced
для объединения кода фрагмент:const mergeObj = require("object-merge-advanced"); const x = { a: { a: 1 } }; const y = { a: { b: 1 } }; const res = console.log(mergeObj(x, y)); // => res = { // a: { // a: 1, // b: 1 // } // }
это алгоритм рекурсивно проходит все ключи входных объектов, сравнивает и строит и возвращает новый объединенный результат.
иногда вам не нужно глубокое слияние, даже если вы так думаете. Например, если у вас есть конфигурация по умолчанию с вложенными объектами, и вы хотите расширить ее глубоко с помощью своей собственной конфигурации, вы можете создать класс для этого. Концепция очень проста:
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]
есть ухоженные библиотеки, которые уже делают это. Одним из примеров в реестре npm является merge-deep
пакет deepmerge npm, по-видимому, является наиболее широко используемой библиотекой для решения этой проблемы: https://www.npmjs.com/package/deepmerge
Я делаю этот метод для глубокого назначения с помощью 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
который основан на pollyfill изObject.assign
on mdn.(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}}}))
Это просто и работает:
let item = { firstName: 'Jonnie', lastName: 'Walker', fullName: function fullName() { return 'Jonnie Walker'; } Object.assign(Object.create(item), item);
объясняю:
Object.create()
создает новый объект. Если вы передаете параметры в функцию, он создает объект прототипом другого объекта. Так что если у вас есть какие-либо функции на прототипе объекта, они будут переданы прототипом другого объекта.
Object.assign()
объединяет два объекта и создает полностью новый объект, и они больше не имеют ссылки. Так что этот пример работает хорошо для меня.