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

И 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() же самое происходит с Object.assign().

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

Ответ 1

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

Нет, это не так.

Ответ 2

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

Вы можете использовать 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 }] }

Ответ 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;
}

Ответ 6

Поскольку этот вопрос все еще активен, здесь есть другой подход:

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

Ответ 7

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

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

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

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

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

Когда я впервые прочитал ответ Салакара, я искренне думал, что могу сделать лучше и проще (вы можете сравнить его с 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 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 от объекта Date или объекта Function.

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

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

Для моего примера я считаю, что только object Object является глубоким, потому что другие объекты, созданные другими конструкторами, могут быть некорректными для углубленного просмотра. Подгоняно из этого SO.

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 own properties into target 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 own properties
            if (options.proto)
                // Copy souce prototype own properties into target prototype 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 } }

Ответ 8

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

Если вы используете ImmutableJS, вы можете использовать mergeDeep:

fromJS(options).mergeDeep(options2).toJS();

Ответ 10

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

Ответ 11

Пакет deepmerge npm является наиболее широко используемой библиотекой для решения этой проблемы: https://www.npmjs.com/package/deepmerge

Ответ 12

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

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

Ответ 13

Я хотел бы представить довольно простой альтернатив ES5. Функция получает 2 параметра - target и source которые должны быть типа "объект". Target объектом будет объект 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 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. array + obj = array
  2. obj + array = obj
  3. obj + obj = obj (рекурсивно слитый)
  4. array + array = array (concat)

Он предсказуем, поддерживает примитивные типы, а также массивы и объекты. Кроме того, поскольку мы можем объединить 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));

Ответ 14

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

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

Ответ 15

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

Если библиотеки npm можно использовать в качестве решения, объект-merge-advanced от вашего действительно позволяет объединить объекты глубоко и настроить/переопределить каждое одно действие слияния с помощью знакомой функции обратного вызова. Основная идея этого - не просто глубокое слияние - что происходит со значением, когда два ключа одинаковы? Эта библиотека позаботится об этом - при столкновении двух ключей object-merge-advanced взвешивает типы, стремясь сохранить как можно больше данных после слияния:

object key merging weighing key value types to retain as much data as possible

Первый ключ входного аргумента отмечен # 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
//      }
//    }

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

Ответ 16

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

$extend (true, target, object)

Ответ 17

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

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

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

Ответ 18

Простое решение с ES5 (перезаписать существующее значение):

function merge(current, update) {
  Object.keys(update).forEach(function(key) {
    // if update[key] exist, and it 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 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));

Ответ 19

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

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

Ответ 20

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

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

Ответ 21

Иногда вам не требуется глубокое слияние, даже если вы так думаете. Например, если у вас есть конфигурация по умолчанию с вложенными объектами, и вы хотите расширить ее с помощью собственной конфигурации, вы можете создать для нее класс. Концепция очень проста:

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

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

Ответ 22

Большинство примеров здесь кажутся слишком сложными, я использую один из созданных мной 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' } } });
    });
  });
});

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

Ответ 23

Рамда, которая является хорошей библиотекой функций 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}}

Ответ 24

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

Документация Object.assign предполагает, что он не делает глубокого клонирования.

Ответ 25

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]

Ответ 26

Есть хорошо сохранившиеся библиотеки, которые уже делают это. Один пример в реестре npm - это слияние

Ответ 27

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

Unit тест:

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

Ответ 28

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

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

Ответ 30

Я делаю этот метод для глубокого присваивания с помощью 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
}