Як глибоке злиття замість неглибокого злиття?


339

І 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, який не повинен бути глибоко скопійований.
riv

122

Ви можете використовувати об'єднання Lodash :

var object = {
  'a': [{ 'b': 2 }, { 'd': 4 }]
};

var other = {
  'a': [{ 'c': 3 }, { 'e': 5 }]
};

_.merge(object, other);
// => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }

6
Гей люди, це найпростіше і найкрасивіше рішення. Лодаш приголомшливий, вони повинні включати його як основний об’єкт js
Нурбол Алпісбаєв

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 разів менший gzipped.
Соло

101

Проблема нетривіальна, якщо мова йде про розміщення об'єктів або будь-якого типу об'єктів, які складніші, ніж мішок цінностей

  • чи викликаєте ви, щоб отримати значення, або скопіюєте його через дескриптор властивості?
  • що робити, якщо ціль об'єднання має сеттер (або власну властивість, або у своєму прототипі)? Чи вважаєте ви значення вже наявним або зателефонуєте сеттеру оновити поточне значення?
  • ви викликаєте функції власності чи копіюєте їх? Що робити, якщо вони пов'язані функції або функції зі стрілками залежно від чогось у ланцюжку їхнього застосування в той час, коли вони були визначені?
  • що робити, якщо це щось на зразок вузла DOM? Ви, звичайно, не хочете ставитися до цього як до простого об'єкта і просто глибоко об'єднати всі його властивості
  • як поводитися з "простими" структурами, такими як масиви, карти чи набори? Вважати їх уже наявними чи зливати їх теж?
  • як поводитися з незліченними власними властивостями?
  • що з новими підметками? Просто призначити посиланням чи глибоким клоном?
  • як поводитися із замороженими / герметичними / нерозширюваними предметами?

Ще одна річ, яку потрібно пам’ятати: об’єктивні графіки, що містять цикли. З цим зазвичай не важко впоратися - просто тримайSet вже відвідувані вихідні об’єкти - але часто забувати.

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

Іншими словами, не існує алгоритму, що відповідає всім розмірам, вам або доведеться прокрутити свій власний або шукати метод бібліотеки, який охоплює ваші випадки використання.


2
виправдання для V8 devs не здійснити безпечну передачу "стану документа"
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висновку 's (я думаю). Наприклад, 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 це питання? Він не мутує входи, але будь-які майбутні мутації на входах можуть мутувати вихід, і навпаки, w / мутації для виведення мутуючих входів. Щодо того, чого варто, однак, у Рамда 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

Славний. Цей також демонструє, що масиви зливаються, що саме я шукав.
Tschallacka

Так, рішення @CplLL, як кажуть, незмінне, але використовує фактичну змінність об'єкта всередині функції при використанні reduce не використовує .
Августин Рідінгер

30

Я знаю, що вже багато відповідей і стільки ж коментарів, що стверджують, що вони не спрацюють. Єдина консенсус полягає в тому, що він настільки складний, що ніхто не робив для нього стандарт . Однак більшість прийнятих відповідей в ТА викривають "прості хитрощі", які широко використовуються. Отже, для всіх нас, як я, які не є експертами, але хочуть написати більш безпечний код, зрозумівши трохи більше про складність JavaScript, я спробую пролити трохи світла.

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

  • [DISCLAIMER] Я пропоную функцію нижче, яка стосується того, як ми заглиблюємося в об'єкти JavaScript для копіювання та ілюструє те, що зазвичай занадто коротко коментується. Це не готово до виробництва. Для ясності я передбачуно залишив осторонь інші міркування, такі як кругові об'єкти (слід за набором чи властивістю символів, що не конфліктують) , копіювання опорного значення або глибокий клон , незмінний об'єкт призначення (знову глибокий клон?), Вивчення конкретного випадку кожен тип об'єктів , отримуйте / встановлюйте властивості за допомогою аксесуарів ... Крім того, я не перевіряв продуктивність - хоча це важливо - тому що тут не справа.
  • Я буду використовувати копію або призначити умови замість злиття . Тому що, на мою думку, злиття є консервативним і повинно провалюватися при конфліктах. Тут, конфліктуючи, ми хочемо, щоб джерело перезаписало призначення. Як і 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'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/ setaccessor, і подібних атрибутів enumerable).

Нарешті, існує багато типів об’єктів . Ви можете по-іншому поводитися з об'єктом Об'єкт від об'єкта Дата або Функція об'єкта.

Отже, написавши свою глибоку копію, вам слід відповісти принаймні на ці запитання:

  1. Що я вважаю глибоким (правильним для рекурсивного погляду) або рівним?
  2. Які властивості я хочу скопіювати? (перелічувані / не перелічувані, рядкові, символічні, власні властивості / власні властивості прототипу, значення / дескриптори ...)

Для свого прикладу я вважаю, що лише object Objects є глибокими , тому що інші об'єкти, створені іншими конструкторами, можуть не бути належними для глибокого огляду. Налаштовані з цього 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};

Пропонована функція

Ви можете перевірити це в цьому планку .

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. масив + obj = масив
  2. obj + масив = obj
  3. obj + obj = obj (рекурсивно об'єднаний)
  4. масив + масив = масив (concat)

Він передбачуваний, підтримує примітивні типи, а також масиви та об'єкти. Крім того, як ми можемо об'єднати 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 - бомба
понеділок74

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

Якщо ви хочете мати один вкладиш, не вимагаючи такої великої бібліотеки, як лодаш, я пропоную вам скористатися 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 позначає злиття двох об'єктів рекурсивно, змінюючи перший.

$ extension (правда, ціль, об'єкт)


9
Запитувач ніколи не вказував, що він використовує jquery, і, схоже, запитує вроджене рішення JavaScript.
Teh JoE

Це дуже простий спосіб зробити це, і це працює. Життєздатне рішення, яке я б розглядав, якби я був тим, хто задає це питання. :)
kashiraja

Це дуже гарна відповідь, але не вистачає посилання на вихідний код до 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

У мене виникла ця проблема під час завантаження кешованого стану редукції. Якщо я просто завантажую кешований стан, я зіткнувся з помилками для нової версії програми з оновленою структурою стану.

Вище було сказано, що лодаш пропонує ту 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
Мені це подобається, але для цього потрібна проста невизначена перевірка на "поточний", інакше {foo: undefined} не зливається. Просто додайте if (current) перед циклом 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

Ramda, яка є приємною бібліотекою функцій 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.