Как глубокое слияние вместо мелкого слияния?


340

И Object.assign, и распространение объекта делают только поверхностное слияние.

Пример проблемы:

// 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().

Есть ли способ сделать это?


такое глубокое слияние, как копирование свойств из одного объекта в другой?

2
Нет, поскольку свойства объекта не должны быть перезаписаны, скорее, каждый дочерний объект должен быть объединен с одним и тем же дочерним объектом на цели, если он уже существует.
Майк

ES6 доработан и новые функции больше не добавляются, AFAIK.
Кангакс


1
@Oriol требует jQuery, хотя ...
m0meni

Ответы:


331

Кто-нибудь знает, существует ли глубокое слияние в спецификации ES6 / ES7?

Нет.


21
Пожалуйста, просмотрите историю изменений. В тот момент, когда я ответил на этот вопрос, кто-нибудь знал, существует ли глубокое слияние в спецификации ES6 / ES7? ,

37
Этот ответ больше не относится к этому вопросу - его следует обновить или удалить
DonVaughn

13
Вопрос не должен был редактироваться до такой степени. Изменения для уточнения. Новый вопрос должен был быть опубликован.
CJ Томпсон

171

Я знаю, что это немного старая проблема, но самое простое решение в ES2015 / ES6, которое я мог придумать, было на самом деле довольно простым, с использованием 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] } } }

Вы найдете неизменную версию этого в ответе ниже.

Обратите внимание, что это приведет к бесконечной рекурсии по циклическим ссылкам. Здесь есть несколько отличных ответов о том, как обнаружить циклические ссылки, если вы думаете, что столкнетесь с этой проблемой.


1
если ваш граф объектов содержит циклы, которые приведут к бесконечной рекурсии
the8472

item !== nullне должен быть нужен внутри isObject, потому что itemуже проверен на правдивость в начале условия
mcont

2
Зачем писать это: Object.assign(target, { [key]: {} })если это может быть просто target[key] = {}?
Юрг Легни

1
... а target[key] = source[key]вместоObject.assign(target, { [key]: source[key] });
Юрг Лени

3
Это не поддерживает любые непрозрачные объекты в target. Например, в mergeDeep({a: 3}, {a: {b: 4}})результате получится дополненный Numberобъект, который явно нежелателен. Кроме того, isObjectне принимает массивы, но принимает любой другой собственный тип объекта, такой как Date, который не должен быть глубоко скопирован.
РИВ

122

Вы можете использовать Lodash Merge :

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 }] }

6
Эй, люди, это самое простое и красивое решение. Lodash великолепен, они должны включать его в качестве основного объекта js
Nurbol Alpysbayev

11
Не должен ли быть результат { 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }?
Дж. Хестерс

Хороший вопрос. Это может быть отдельный вопрос или вопрос для тех, кто поддерживает Лодаш.
Эндрю Хендерсон

7
Результат { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }правильный, потому что мы объединяем элементы массива. Элемент 0из object.aIS {b: 2}, элемент 0из other.aэто {c: 3}. Когда эти два слиты из-за того, что они имеют один и тот же индекс массива, результатом является { 'b': 2, 'c': 3 }элемент 0нового объекта.
Александру Фуркулита

Я предпочитаю этот , он в 6 раз меньше сжатого.
Соло

101

Проблема нетривиальна, когда речь идет о хост-объектах или объектах любого вида, более сложных, чем пакет значений.

  • Вы вызываете метод получения для получения значения или копируете дескриптор свойства?
  • Что делать, если цель слияния имеет установщик (либо собственное свойство, либо в своей цепочке прототипов)? Считаете ли вы значение уже существующим или вызываете установщик для обновления текущего значения?
  • Вы вызываете функции собственного свойства или копируете их? Что, если они являются связанными функциями или функциями стрелок, зависящими от чего-то в их цепочке областей действия во время их определения?
  • что если это что-то вроде узла DOM? Вы, конечно, не хотите рассматривать его как простой объект и просто объединить все его свойства в
  • как бороться с «простыми» структурами, такими как массивы, карты или множества? Считать их уже присутствующими или объединить их тоже?
  • как бороться с не перечисляемыми собственными свойствами?
  • как насчет новых поддеревьев? Просто назначить по ссылке или глубоко клонировать?
  • как бороться с замороженными / запечатанными / нерастяжимыми объектами?

Еще одна вещь, которую нужно иметь в виду: графы объектов, которые содержат циклы. Это обычно не сложно иметь дело - просто держитеSet уже посещенные исходные объекты - но часто забывают.

Вам, вероятно, следует написать функцию глубокого слияния, которая ожидает только примитивные значения и простые объекты - в большинстве случаев те типы, которые может обрабатывать алгоритм структурированного клона - в качестве источников слияния. Бросьте, если он сталкивается с чем-то, что он не может обработать или просто назначить по ссылке вместо глубокого слияния.

Другими словами, не существует единого алгоритма, подходящего для всех, вы должны либо свернуть свой собственный, либо искать библиотечный метод, который подходит для ваших вариантов использования.


2
оправдания для разработчиков V8, чтобы не осуществить безопасную передачу "состояния документа"
neaumusic

Вы поднимаете много хороших вопросов, и я бы хотел увидеть выполнение вашей рекомендации. Поэтому я попытался сделать один ниже. Не могли бы вы взглянуть и прокомментировать? stackoverflow.com/a/48579540/8122487
RaphaMex

66

Вот неизменяемая (не изменяющая входные данные) версия ответа @ 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;
}

1
@torazaburo, см. предыдущий мною пост о функции
isObject

обновил его. после некоторого тестирования я нашел ошибку с глубоко вложенными объектами
CpILL

3
Это вычисляемое имя свойства, первое будет использовать значение в keyкачестве имени свойства, позднее будет делать «ключ» именем свойства. См. Es6-features.org/#ComputedPropertyNames
CpILL,

2
в isObjectвам не нужно проверить && item !== nullв конце, потому что начинается линия с item &&, нет?
Эфемер

2
Если источник имеет вложенные дочерние объекты глубже, чем цель, эти объекты будут по-прежнему ссылаться на те же значения в mergedDeepвыводе (я думаю). Например, const target = { a: 1 }; const source = { b: { c: 2 } }; const merged = mergeDeep(target, source); merged.b.c; // 2 source.b.c = 3; merged.b.c; // 3 это проблема? Он не мутирует входы, но любые будущие мутации на входах могут изменять выход, и наоборот, с мутациями для вывода изменяющихся входов. Что бы это ни стоило, ramda R.merge()имеет такое же поведение.
Джеймс Конклинг

40

Так как эта проблема все еще активна, вот другой подход:

  • 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);


Это хорошо. Однако, когда у нас есть массив с повторяющимися элементами, они объединяются (есть повторяющиеся элементы). Я адаптировал это, чтобы принять параметр (массивы уникальные: true / false).
Астронавт

1
Чтобы сделать массивы уникальными, вы можете изменить их prev[key] = pVal.concat(...oVal);наprev[key] = [...pVal, ...oVal].filter((element, index, array) => array.indexOf(element) === index);
Ричард Херрис

1
Так красиво и чисто! Определенно лучший ответ здесь!
538ROMEO

Славный. Этот демонстрирует также, что массивы объединяются, что я и искал.
Чаллака

Да, решение @CplLL называется неизменным, но использует фактическую изменяемость объекта внутри функции, а использование reduce - нет.
Августин Ридингер

30

Я знаю, что ответов уже много и столько комментариев, что они не сработают. Единственный консенсус в том, что это настолько сложно, что никто не сделал для него стандарта . Тем не менее, большинство принятых ответов в SO раскрывают «простые трюки», которые широко используются. Итак, для всех нас, таких как я, которые не являются экспертами, но хотят писать более безопасный код, поняв немного больше о сложности javascript, я постараюсь пролить немного света.

Прежде чем испачкать руки, позвольте мне уточнить 2 момента:

  • [ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ] Я предлагаю функцию ниже, которая рассматривает, как мы углубляем цикл в объекты javascript для копирования и иллюстрирует то, что обычно слишком коротко комментируется. Это не готово к производству. Для ясности я намеренно оставил в стороне другие соображения, такие как круговые объекты (отслеживание с помощью свойства набора или неконфликтующего символа) , копирование эталонного значения или глубокого клона , неизменяемый целевой объект (снова глубокий клон?), Индивидуальное изучение каждый тип объектов , получение / установка свойств с помощью аксессоров ... Кроме того, я не тестировал производительность - хотя это важно - потому что здесь тоже не главное.
  • Я буду использовать копирование или назначение терминов вместо слияния . Потому что, на мой взгляд, слияние является консервативным и должно провалиться при конфликтах. Здесь, при конфликте, мы хотим, чтобы источник перезаписал место назначения. Как и Object.assignделает.

Ответы с for..inили Object.keysвводят в заблуждение

Создание глубокой копии кажется настолько простой и обычной практикой, что мы ожидаем найти однострочник или, по крайней мере, быстрый выигрыш с помощью простой рекурсии. Мы не ожидаем, что нам нужна библиотека или написать пользовательскую функцию из 100 строк.

Когда я впервые прочитал ответ Salakar в , я действительно думал , что я мог бы сделать лучше и проще (вы можете сравнить его с Object.assignна x={a:1}, y={a:{b:1}}). Затем я прочитал ответ «8472» и подумал ... что так легко не уйти, улучшение уже полученных ответов не даст нам далеко.

Давайте на мгновение оставим глубокое копирование и рекурсивность. Просто подумайте, как (неправильно) люди анализируют свойства, чтобы скопировать очень простой объект.

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аксессора, и атрибутов типа enumerable).

Наконец, есть много типов объектов . Вы можете по-разному обрабатывать объект Object из объекта Date или объекта Function.

Итак, написав свою глубокую копию, вы должны ответить хотя бы на следующие вопросы:

  1. Что я считаю глубоким (подходящим для рекурсивного поиска) или плоским?
  2. Какие свойства я хочу скопировать? (перечислимые / не перечислимые, строковые / символьные, собственные свойства / собственные свойства прототипа, значения / дескрипторы ...)

В моем примере я считаю, что только object Objects являются глубокими , потому что другие объекты, созданные другими конструкторами, могут не подходить для углубленного анализа. Настроены из этого ТАК .

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};

Предлагаемая функция

Вы можете проверить это в этом поршне .

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 } }

13

Я использую lodash:

import _ = require('lodash');
value = _.merge(value1, value2);

2
Обратите внимание, что слияние изменит объект, если вы хотите что-то, что не изменяет объект, то _cloneDeep(value1).merge(value2)
geckos

3
@geckos Вы можете сделать _.merge ({}, value1, value2)
Spenhouet

10

Вот реализация 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*/}
}

9

Вот еще одно решение 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
}

3
это проверено и / или является частью библиотеки, выглядит красиво, но хотелось бы убедиться, что это несколько доказано.


8

Я хотел бы представить довольно простую альтернативу 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 остающийся случай), то происходит рекурсия, объединяющая два объекта (или объединение двух массивов);

Также учтите следующее :

  1. массив + объект = массив
  2. obj + array = obj
  3. obj + obj = obj (рекурсивно объединено)
  4. массив + массив = массив (конкат)

Это предсказуемо, поддерживает примитивные типы, а также массивы и объекты. Кроме того, как мы можем объединить 2 объектов, я думаю , что мы можем объединить более 2 с помощью снижения функции.

взгляните на пример (и поиграйте с ним, если хотите) :

var a = {
   "a_prop": 1,
   "arr_prop": [4, 5, 6],
   "obj": {
     "a_prop": {
       "t_prop": 'test'
     },
     "b_prop": 2
   }
};

var b = {
   "a_prop": 5,
   "arr_prop": [7, 8, 9],
   "b_prop": 15,
   "obj": {
     "a_prop": {
       "u_prop": false
     },
     "b_prop": {
        "s_prop": null
     }
   }
};

function deepMerge(target, source) {
    if(typeof target !== 'object' || typeof source !== 'object') return false;
    for(var prop in source) {
    if(!source.hasOwnProperty(prop)) continue;
      if(prop in target) {
        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) {
              target[prop] = target[prop].concat(source[prop]);
            } else {
              target[prop] = deepMerge(target[prop], source[prop]); 
            } 
          }  
        }
      } else {
        target[prop] = source[prop]; 
      }
    }
  return target;
}

console.log(deepMerge(a, b));

Есть ограничение - длина стека вызовов браузера. Современные браузеры выдадут ошибку на очень глубоком уровне рекурсии (например, тысячи вложенных вызовов). Также вы можете свободно обрабатывать ситуации, такие как массив + объект и т. Д., Добавляя новые условия и проверки типов.



7

Есть ли способ сделать это?

Если библиотеки npm могут быть использованы в качестве решения, объектно-расширенный объект вашей компании действительно позволяет глубоко объединять объекты и настраивать / переопределять каждое действие слияния, используя знакомую функцию обратного вызова. Основная идея этого заключается не только в глубоком слиянии - что происходит со значением, когда два ключа совпадают ? Эта библиотека заботится об этом - когда два ключа сталкиваются, 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
//      }
//    }

Его алгоритм рекурсивно обходит все входные ключи объекта, сравнивает и строит и возвращает новый объединенный результат.


6

Следующая функция делает глубокую копию объектов, она охватывает копирование примитивов, массивов, а также объектов

 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;
}

6

Простое решение с 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 вызывал проблемы при сборке - эта альтернатива
es5

5

Большинство примеров здесь кажутся слишком сложными, я использую один из созданных мной TypeScript, думаю, он должен охватывать большинство случаев (я обрабатываю массивы как обычные данные, просто заменяя их).

const isObject = (item: any) => typeof item === 'object' && !Array.isArray(item);

export const merge = <A = Object, B = Object>(target: A, source: B): A & B => {
  const isDeep = (prop: string) =>
    isObject(source[prop]) && target.hasOwnProperty(prop) && isObject(target[prop]);
  const replaced = Object.getOwnPropertyNames(source)
    .map(prop => ({ [prop]: isDeep(prop) ? merge(target[prop], source[prop]) : source[prop] }))
    .reduce((a, b) => ({ ...a, ...b }), {});

  return {
    ...(target as Object),
    ...(replaced as Object)
  } as A & B;
};

То же самое в обычном JS, на всякий случай:

const isObject = item => typeof item === 'object' && !Array.isArray(item);

const merge = (target, source) => {
  const isDeep = prop => 
    isObject(source[prop]) && target.hasOwnProperty(prop) && isObject(target[prop]);
  const replaced = Object.getOwnPropertyNames(source)
    .map(prop => ({ [prop]: isDeep(prop) ? merge(target[prop], source[prop]) : source[prop] }))
    .reduce((a, b) => ({ ...a, ...b }), {});

  return {
    ...target,
    ...replaced
  };
};

Вот мои тестовые примеры, чтобы показать, как вы могли бы использовать его

describe('merge', () => {
  context('shallow merges', () => {
    it('merges objects', () => {
      const a = { a: 'discard' };
      const b = { a: 'test' };
      expect(merge(a, b)).to.deep.equal({ a: 'test' });
    });
    it('extends objects', () => {
      const a = { a: 'test' };
      const b = { b: 'test' };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: 'test' });
    });
    it('extends a property with an object', () => {
      const a = { a: 'test' };
      const b = { b: { c: 'test' } };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: { c: 'test' } });
    });
    it('replaces a property with an object', () => {
      const a = { b: 'whatever', a: 'test' };
      const b = { b: { c: 'test' } };
      expect(merge(a, b)).to.deep.equal({ a: 'test', b: { c: 'test' } });
    });
  });

  context('deep merges', () => {
    it('merges objects', () => {
      const a = { test: { a: 'discard', b: 'test' }  };
      const b = { test: { a: 'test' } } ;
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: 'test' } });
    });
    it('extends objects', () => {
      const a = { test: { a: 'test' } };
      const b = { test: { b: 'test' } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: 'test' } });
    });
    it('extends a property with an object', () => {
      const a = { test: { a: 'test' } };
      const b = { test: { b: { c: 'test' } } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: { c: 'test' } } });
    });
    it('replaces a property with an object', () => {
      const a = { test: { b: 'whatever', a: 'test' } };
      const b = { test: { b: { c: 'test' } } };
      expect(merge(a, b)).to.deep.equal({ test: { a: 'test', b: { c: 'test' } } });
    });
  });
});

Пожалуйста, дайте мне знать, если вы думаете, что мне не хватает какой-то функциональности.


5

Если вы хотите иметь один лайнер, не требуя огромной библиотеки, такой как lodash, я предлагаю вам использовать deepmerge . ( npm install deepmerge)

Затем вы можете сделать

deepmerge({ a: 1, b: 2, c: 3 }, { a: 2, d: 3 });

получить

{ a: 2, b: 2, c: 3, d: 3 }

Хорошая вещь, это идет с наборами для TypeScript сразу. Это также позволяет объединять массивы . Это действительно универсальное решение.


4

Мы можем использовать $ .extend (true, object1, object2) для глубокого слияния. Значение true обозначает слияние двух объектов рекурсивно, изменяя первый.

$ Простираться (правда, цель, объект)


9
Запрашивающий никогда не указывал, что они используют jquery, и, похоже, запрашивает нативное решение javascript.
Teh JOE

Это очень простой способ сделать это, и это работает. Жизнеспособное решение, которое я бы рассмотрел, если бы я задавал этот вопрос. :)
Кашираджа

Это очень хороший ответ, но отсутствует ссылка на исходный код для jQuery. В jQuery много людей, работающих над проектом, и они потратили некоторое время на то, чтобы сделать правильное копирование. Кроме того, исходный код довольно «прост»: github.com/jquery/jquery/blob/master/src/core.js#L125 «Простой» заключен в кавычки, поскольку он начинает усложняться при копании jQuery.isPlainObject(). Это раскрывает сложность определения того, является ли что-то простым объектом, который большинство ответов здесь упускает из виду. Угадайте, на каком языке написан jQuery?
CubicleSoft

4

Здесь прямое, простое решение, которое работает как Object.assignпросто глубже и работает для массива, без каких-либо изменений

function deepAssign(target, ...sources) {
    for( source of sources){
        for(let k in source){
            let vs = source[k], vt = target[k];
            if(Object(vs)== vs && Object(vt)===vt ){
                target[k] = deepAssign(vt, vs)
                continue;
            }
            target[k] = source[k];
        }    
    }
    return target;
}

пример

x = { a: { a: 1 }, b:[1,2] };
y = { a: { b: 1 }, b:[3] };
z = {c:3,b:[,,,4]}
x = deepAssign(x,y,z)
// x will be
x ==  {
  "a": {
    "a": 1,
    "b": 1
  },
  "b": [    1,    2,    null,    4  ],
  "c": 3
}


3

У меня была эта проблема при загрузке кэшированного избыточного состояния. Если бы я просто загружал кэшированное состояние, я столкнулся бы с ошибками для новой версии приложения с обновленной структурой состояний.

Уже упоминалось, что lodash предлагает mergeфункцию, которую я использовал:

const currentInitialState = configureState().getState();
const mergedState = _.merge({}, currentInitialState, cachedState);
const store = configureState(mergedState);

3

Многие ответы используют десятки строк кода или требуют добавления новой библиотеки в проект, но если вы используете рекурсию, это всего лишь 4 строки кода.

function merge(current, updates) {
  for (key of Object.keys(updates)) {
    if (!current.hasOwnProperty(key) || typeof updates[key] !== 'object') current[key] = updates[key];
    else merge(current[key], updates[key]);
  }
  return current;
}
console.log(merge({ a: { a: 1 } }, { a: { b: 1 } }));

Обработка массивов: вышеприведенная версия заменяет старые значения массива новыми. Если вы хотите, чтобы он сохранил старые значения массива и добавил новые, просто добавьте else if (current[key] instanceof Array && updates[key] instanceof Array) current[key] = current[key].concat(updates[key])блок над elseотчетливостью, и все готово.


1
Мне это нравится, но нужна простая неопределенная проверка 'current', иначе {foo: undefined} не сливается. Просто добавьте if (текущий) перед циклом for.
Андреас Пардейке

Спасибо за предложение
Винсент

2

Вот еще один, который я только что написал, который поддерживает массивы. Это их объединяет.

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);
};

2

Используйте эту функцию:

merge(target, source, mutable = false) {
        const newObj = typeof target == 'object' ? (mutable ? target : Object.assign({}, target)) : {};
        for (const prop in source) {
            if (target[prop] == null || typeof target[prop] === 'undefined') {
                newObj[prop] = source[prop];
            } else if (Array.isArray(target[prop])) {
                newObj[prop] = source[prop] || target[prop];
            } else if (target[prop] instanceof RegExp) {
                newObj[prop] = source[prop] || target[prop];
            } else {
                newObj[prop] = typeof source[prop] === 'object' ? this.merge(target[prop], source[prop]) : source[prop];
            }
        }
        return newObj;
    }

2

Рамда, которая является хорошей библиотекой функций JavaScript, имеет mergeDeepLeft и mergeDeepRight. Любой из них работает очень хорошо для этой проблемы. Пожалуйста, ознакомьтесь с документацией здесь: https://ramdajs.com/docs/#mergeDeepLeft

Для конкретного рассматриваемого примера мы можем использовать:

import { mergeDeepLeft } from 'ramda'
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = mergeDeepLeft(x, y)) // {"a":{"a":1,"b":1}}

2
// copies all properties from source object to dest object recursively
export function recursivelyMoveProperties(source, dest) {
  for (const prop in source) {
    if (!source.hasOwnProperty(prop)) {
      continue;
    }

    if (source[prop] === null) {
      // property is null
      dest[prop] = source[prop];
      continue;
    }

    if (typeof source[prop] === 'object') {
      // if property is object let's dive into in
      if (Array.isArray(source[prop])) {
        dest[prop] = [];
      } else {
        if (!dest.hasOwnProperty(prop)
        || typeof dest[prop] !== 'object'
        || dest[prop] === null || Array.isArray(dest[prop])
        || !Object.keys(dest[prop]).length) {
          dest[prop] = {};
        }
      }
      recursivelyMoveProperties(source[prop], dest[prop]);
      continue;
    }

    // property is simple type: string, number, e.t.c
    dest[prop] = source[prop];
  }
  return dest;
}

Модульный тест:

describe('recursivelyMoveProperties', () => {
    it('should copy properties correctly', () => {
      const source: any = {
        propS1: 'str1',
        propS2: 'str2',
        propN1: 1,
        propN2: 2,
        propA1: [1, 2, 3],
        propA2: [],
        propB1: true,
        propB2: false,
        propU1: null,
        propU2: null,
        propD1: undefined,
        propD2: undefined,
        propO1: {
          subS1: 'sub11',
          subS2: 'sub12',
          subN1: 11,
          subN2: 12,
          subA1: [11, 12, 13],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
        propO2: {
          subS1: 'sub21',
          subS2: 'sub22',
          subN1: 21,
          subN2: 22,
          subA1: [21, 22, 23],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      };
      let dest: any = {
        propS2: 'str2',
        propS3: 'str3',
        propN2: -2,
        propN3: 3,
        propA2: [2, 2],
        propA3: [3, 2, 1],
        propB2: true,
        propB3: false,
        propU2: 'not null',
        propU3: null,
        propD2: 'defined',
        propD3: undefined,
        propO2: {
          subS2: 'inv22',
          subS3: 'sub23',
          subN2: -22,
          subN3: 23,
          subA2: [5, 5, 5],
          subA3: [31, 32, 33],
          subB2: false,
          subB3: true,
          subU2: 'not null --- ',
          subU3: null,
          subD2: ' not undefined ----',
          subD3: undefined,
        },
        propO3: {
          subS1: 'sub31',
          subS2: 'sub32',
          subN1: 31,
          subN2: 32,
          subA1: [31, 32, 33],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      };
      dest = recursivelyMoveProperties(source, dest);

      expect(dest).toEqual({
        propS1: 'str1',
        propS2: 'str2',
        propS3: 'str3',
        propN1: 1,
        propN2: 2,
        propN3: 3,
        propA1: [1, 2, 3],
        propA2: [],
        propA3: [3, 2, 1],
        propB1: true,
        propB2: false,
        propB3: false,
        propU1: null,
        propU2: null,
        propU3: null,
        propD1: undefined,
        propD2: undefined,
        propD3: undefined,
        propO1: {
          subS1: 'sub11',
          subS2: 'sub12',
          subN1: 11,
          subN2: 12,
          subA1: [11, 12, 13],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
        propO2: {
          subS1: 'sub21',
          subS2: 'sub22',
          subS3: 'sub23',
          subN1: 21,
          subN2: 22,
          subN3: 23,
          subA1: [21, 22, 23],
          subA2: [],
          subA3: [31, 32, 33],
          subB1: false,
          subB2: true,
          subB3: true,
          subU1: null,
          subU2: null,
          subU3: null,
          subD1: undefined,
          subD2: undefined,
          subD3: undefined,
        },
        propO3: {
          subS1: 'sub31',
          subS2: 'sub32',
          subN1: 31,
          subN2: 32,
          subA1: [31, 32, 33],
          subA2: [],
          subB1: false,
          subB2: true,
          subU1: null,
          subU2: null,
          subD1: undefined,
          subD2: undefined,
        },
      });
    });
  });

2

Я нашел только двухстрочное решение для глубокого слияния в javascript. Дайте мне знать, как это работает для вас.

const obj1 = { a: { b: "c", x: "y" } }
const obj2 = { a: { b: "d", e: "f" } }
temp = Object.assign({}, obj1, obj2)
Object.keys(temp).forEach(key => {
    temp[key] = (typeof temp[key] === 'object') ? Object.assign(temp[key], obj1[key], obj2[key]) : temp[key])
}
console.log(temp)

Temp объект напечатает {a: {b: 'd', e: 'f', x: 'y'}}


1
Это не делает глубокое слияние. Это не удастся с merge({x:{y:{z:1}}}, {x:{y:{w:2}}}). Il также не сможет обновить существующие значения в obj1, если у obj2 они тоже есть, например, с помощью merge({x:{y:1}}, {x:{y:2}}).
Орейль

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);

Вы можете преобразовать его в функцию (не конструктор).


1

Это дешевое глубокое слияние, в котором используется как можно меньше кода. Каждый источник перезаписывает предыдущее свойство, когда оно существует.

const { keys } = Object;

const isObject = a => typeof a === "object" && !Array.isArray(a);
const merge = (a, b) =>
  isObject(a) && isObject(b)
    ? deepMerge(a, b)
    : isObject(a) && !isObject(b)
    ? a
    : b;

const coalesceByKey = source => (acc, key) =>
  (acc[key] && source[key]
    ? (acc[key] = merge(acc[key], source[key]))
    : (acc[key] = source[key])) && acc;

/**
 * Merge all sources into the target
 * overwriting primitive values in the the accumulated target as we go (if they already exist)
 * @param {*} target
 * @param  {...any} sources
 */
const deepMerge = (target, ...sources) =>
  sources.reduce(
    (acc, source) => keys(source).reduce(coalesceByKey(source), acc),
    target
  );

console.log(deepMerge({ a: 1 }, { a: 2 }));
console.log(deepMerge({ a: 1 }, { a: { b: 2 } }));
console.log(deepMerge({ a: { b: 2 } }, { a: 1 }));

1

Я использую следующую короткую функцию для глубокого объединения объектов.
Это прекрасно работает для меня.
Автор полностью объясняет, как это работает здесь.

/*!
 * Merge two or more objects together.
 * (c) 2017 Chris Ferdinandi, MIT License, https://gomakethings.com
 * @param   {Boolean}  deep     If true, do a deep (or recursive) merge [optional]
 * @param   {Object}   objects  The objects to merge together
 * @returns {Object}            Merged values of defaults and options
 * 
 * Use the function as follows:
 * let shallowMerge = extend(obj1, obj2);
 * let deepMerge = extend(true, obj1, obj2)
 */

var extend = function () {

    // Variables
    var extended = {};
    var deep = false;
    var i = 0;

    // Check if a deep merge
    if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
        deep = arguments[0];
        i++;
    }

    // Merge the object into the extended object
    var merge = function (obj) {
        for (var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                // If property is an object, merge properties
                if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') {
                    extended[prop] = extend(extended[prop], obj[prop]);
                } else {
                    extended[prop] = obj[prop];
                }
            }
        }
    };

    // Loop through each object and conduct a merge
    for (; i < arguments.length; i++) {
        merge(arguments[i]);
    }

    return extended;

};

Хотя эта ссылка может ответить на вопрос, лучше включить сюда основные части ответа и предоставить ссылку для справки. Ответы, содержащие только ссылки, могут стать недействительными, если связанная страница изменится. - Из обзора
Крис Камаратта

Привет @ChrisCamaratta. Здесь важна не только важная часть, но и вся функция - как ее использовать. Так что это определенно не только ссылка. Это функция, которую я использовал для глубокого объединения объектов. Ссылка есть только в том случае, если вы хотите, чтобы автор объяснил, как она работает. Я чувствую, что это было бы плохой услугой для сообщества, чтобы попытаться объяснить работу лучше, чем автор, который преподает JavaScript. Спасибо за комментарий.
Джон Ширинг

Да. Либо я пропустил его, либо код не появился в интерфейсе рецензента, когда я его просматривал. Я согласен, что это качественный ответ. Казалось бы, другие рецензенты отвергли мою первоначальную оценку, так что я думаю, что вы в порядке. Извините за флаг вдохновения.
Крис Камаратта

Большой! @ChrisCamaratta, спасибо, что помогли мне понять, что случилось.
Джон Ширинг
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.