浅いマージの代わりに深いマージする方法は?


337

Object.assignObject 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は完全に上書きされます。これは、spread構文が1レベルだけ深くなるためです。これはと同じObject.assign()です。

これを行う方法はありますか?


深いマージは、あるオブジェクトから別のオブジェクトにプロパティをコピーすることと同じですか?

2
いいえ、オブジェクトのプロパティは上書きしないでください。子オブジェクトがすでに存在する場合は、子オブジェクトをターゲット上の同じ子にマージする必要があります。
Mike

ES6が完成し、新機能が追加されなくなりました。
kangax


1
@OriolにはjQueryが必要です...
m0meni

回答:


330

ES6 / ES7仕様にディープマージが存在するかどうか誰かが知っていますか?

いいえ、違います。


21
編集履歴を確認してください。私がこれに答えたときの質問は、ES6 / ES7仕様に深いマージが存在するかどうか誰かが知っていますか?

37
この回答はこの質問には適用されません-更新または削除する必要があります
DonVaughn

13
質問はこの程度に編集されるべきではありません。編集は明確にするためのものです。新しい質問が投稿されているはずです。
CJトンプソン、

170

私はこれが少し古い問題であることを知っていますが、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

2
なぜこれを書く:Object.assign(target, { [key]: {} })それは単にすることができればtarget[key] = {}
ユルクLehni

1
...そしてtarget[key] = source[key]代わりにObject.assign(target, { [key]: source[key] });
ユルクLehni

3
これは、のプレーンでないオブジェクトをサポートしていませんtarget。たとえば、オブジェクトがmergeDeep({a: 3}, {a: {b: 4}})拡張されNumberますが、これは明らかに望ましくありません。また、isObject配列は受け入れませんが、などのその他のネイティブオブジェクトタイプを受け入れますDate
2017

2
私が理解しているように配列では動作しませんか?
Vedmant

119

あなたは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
こんにちは、これは最も単純で最も美しい解決策です。Lodashは素晴らしいです。コアjsオブジェクトとして含める必要があります
Nurbol Alpysbayev

11
結果は{ 'a': [{ 'b': 2 }, { 'c': 3 }, { 'd': 4 }, { 'e': 5 }] }どうですか?
J.ヘスターズ2018

良い質問。それは別の質問かもしれませんし、Lodashのメンテナにとっても一つの質問かもしれません。
AndrewHenderson

7
{ 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] }配列の要素をマージしているので、結果は正しいです。要素0object.aISは{b: 2}、要素0のです。これら2つが同じ配列インデックスを持つためにマージされると、結果はになります。これは、新しいオブジェクトの要素です。other.a{c: 3}{ 'b': 2, 'c': 3 }0
Alexandru Furculita

私はこれを好む、それは6x小さいgzip圧縮されています。
ソロ

101

ホストオブジェクトや、値のバッグよりも複雑なあらゆる種類のオブジェクトに関しては、問題は自明ではありません

  • 値を取得するためにゲッターを呼び出しますか、それともプロパティ記述子をコピーしますか?
  • マージターゲットにセッター(独自のプロパティまたはそのプロトタイプチェーンのいずれか)がある場合はどうなりますか?値はすでに存在すると見なしますか、それともセッターを呼び出して現在の値を更新しますか?
  • 自分のプロパティ関数を呼び出すか、それらをコピーしますか?それらが定義されたときにスコープチェーン内の何かに依存して、それらがバインドされた関数または矢印関数である場合はどうなりますか?
  • DOMノードのような場合はどうなりますか?あなたは確かにそれを単純なオブジェクトとして扱いたくありません、そしてその中にすべてのプロパティを深くマージします
  • 配列、マップ、またはセットのような「単純な」構造をどのように扱うか?それらがすでに存在するか、それらをマージするかを検討しますか?
  • 列挙できない独自のプロパティを処理する方法?
  • 新しいサブツリーはどうですか?参照またはディープクローンで割り当てるだけですか?
  • 冷凍/封印/非拡張オブジェクトをどのように扱うのですか?

もう1つ覚えておかなければならないのは、サイクルを含むオブジェクトグラフです。通常、対処することは難しくありません。Setすでにアクセスしたソースオブジェクトをですが、しばしば忘れられます。

プリミティブ値と単純なオブジェクトのみを期待するディープマージ関数を作成する必要があります-ほとんどの場合、構造化クローンアルゴリズムで処理できるのみをマージソースとしてがあります。深いマージの代わりに、処理できない、または参照によって割り当てることができない何かに遭遇した場合にスローします。

言い換えれば、万能のアルゴリズムはありません。自分でロールするか、ユースケースをカバーするライブラリメソッドを探す必要があります。


2
V8開発者が安全な「ドキュメントステート」転送を実装しないことの言い訳
neaumusic

あなたは多くの良い問題を提起し、私はあなたの推薦の実装を見てみたいと思いました。なので、以下で作ってみました。見てコメントしていただけませんか?stackoverflow.com/a/48579540/8122487
RaphaMex 2018

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関数についての私による以前の投稿を参照してください
Salakar

更新しました。いくつかのテストの後、深くネストされたオブジェクトのバグを発見しました
CpILL

3
その計算されたプロパティ名で、最初はkeyプロパティ名としての値を使用し、後者は「キー」をプロパティ名にします。参照:es6-features.org/#ComputedPropertyNames
CpILL

2
isObjectあなたをチェックする必要はありません&& item !== null行で始まるので、最後にitem &&なし、?
エフェマ2016年

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()動作は同じです。
James Conkling

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);
リチャードHerries

1
とても素敵できれいです!! 間違いなくここで最高の答え!
538ROMEO

輝かしい。これは、配列がマージされることも示しています。これは私が探していたものです。
Tschallacka

はい、@ CplLLソリューションは不変であると言われていますが、関数内では実際のオブジェクトの可変性を使用reduce していますが、そうではありません。
オーガスティンリーディンガー

30

私はすでに多くの回答があり、それらが機能しないと主張する多くのコメントがあることを知っています。唯一の合意は、複雑すぎて誰も標準を作らなかったということですです。ただし、SOで受け入れられている回答のほとんどは、広く使用されている「単純なトリック」を公開しています。したがって、エキスパートではないがJavaScriptの複雑さについてもう少し把握して、より安全なコードを書きたいと思っている私たちすべてにとって、私はいくつかの光を当てようとします。

手を汚す前に、2つの点を明確にしましょう。

  • [免責事項]以下の関数を提案して、コピーのためにJavaScriptオブジェクトにディープループする方法に取り組み、コメントが短すぎることを示します。本番環境では使用できません。わかりやすくするために、私は意図的に、循環オブジェクト(セットまたは競合しないシンボルプロパティで追跡)、参照値またはディープクローンのコピー、不変の宛先オブジェクト(再度ディープクローン?)、各タイプのオブジェクトアクセサを介したプロパティの取得/設定...また、パフォーマンスもテストしませんでした。重要ですが、ここでも重要ではありません。
  • マージではなく、コピーまたは割り当ての用語を使用します。私の考えでは、マージは保守的であり、衝突で失敗するはずです。ここで、競合する場合、ソースが宛先を上書きするようにします。のように。Object.assign

誤解を招く、for..inまたはObject.keys誤解を招く回答

深いコピーを作成することは非常に基本的で一般的な慣行のように思えるため、ワンライナー、または少なくとも、単純な再帰による迅速な成功を期待しています。ライブラリが必要だとか、100行のカスタム関数を書いたりするとは思いません。

私が最初にサラカーの回答を読んだとき、私は本当にもっと上手く、もっと簡単にできると思っていました(あなたはそれをObject.assignon と比較できます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.getOwnPropertyDescriptorsObject.getOwnPropertyNames、おそらくそれが間違っています。Object.getOwnPropertySymbolsObject.getPrototypeOf

関数を書く前に考慮すべきこと

まず、JavaScriptオブジェクトが何であるかを理解していることを確認してください。JavaScriptでは、オブジェクトは独自のプロパティと(親)プロトタイプオブジェクトで構成されます。プロトタイプオブジェクトは、独自のプロパティとプロトタイプオブジェクトで構成されています。など、プロトタイプチェーンを定義します。

プロパティは、キー(stringまたはsymbol)と記述子(valueまたはget/ setアクセサ、およびのような属性)のペアですenumerable

最後に、オブジェクトには多くのタイプがあります。オブジェクト日付またはオブジェクト関数とは異なるオブジェクトオブジェクトを処理する必要がある場合があります。

したがって、深いコピーを作成する場合は、少なくとも次の質問に答える必要があります。

  1. ディープ(再帰的なルックアップに適切)またはフラットとは何ですか?
  2. コピーしたいプロパティは何ですか?(列挙可能/列挙不可、文字列キー/記号キー、独自のプロパティ/プロトタイプ独自のプロパティ、値/記述子...)

私の例では、他のコンストラクターによって作成された他のオブジェクトは詳細な外観に適さない可能性があるため、object Objects のみがdeepであると考えます。この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

私はロダッシュを使います:

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

2
あなたがオブジェクトを変異させていない何かをしたい場合は、マージは、その後、オブジェクトを変更することに注意してください _cloneDeep(value1).merge(value2)
ヤモリ

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

deepmerge npmパッケージは、この問題を解決するために最も広く使用されているライブラリのようです:https ://www.npmjs.com/package/deepmerge


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がない場合は取得します。sourcetarget
  • 場合target持っているsourceプロパティおよびtargetsource両方のオブジェクト(4のうち3例)がない、targetのプロパティが上書きされます。
  • 場合target持っているsourceプロパティおよびそれらの両方をオブジェクト/アレイ(1残っている場合)には、再帰(または2つの配列の連結)2つのオブジェクトをマージ起こるです。

次のことも考慮してください。

  1. 配列+ obj =配列
  2. obj +配列= obj
  3. obj + obj = obj(再帰的にマージ)
  4. 配列+配列=配列(連結)

予測可能であり、プリミティブ型、配列、オブジェクトをサポートしています。また、2つのオブジェクトをマージできるので、reduce関数を使用して3つ以上のオブジェクトをマージできると思います。

例を見てみましょう(そして、必要に応じてそれをいじってください)

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

制限があります-ブラウザの呼び出しスタックの長さ。最近のブラウザーは、非常に深いレベルの再帰でエラーをスローします(何千ものネストされた呼び出しを考えてください)。また、新しい条件とタイプチェックを追加することで、配列+オブジェクトなどの状況を自由に扱うことができます。


8

これは別の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
これはテスト済みまたはライブラリの一部、あるいはその両方です。見た目はいいですが、それがいくらか証明されていることを確認したいと思います。


7

これを行う方法はありますか?

npmライブラリをソリューションとして使用できる場合、オブジェクトからの高度なオブジェクトマージにより、オブジェクトを深くマージし、使い慣れたコールバック関数を使用してすべてのマージアクションをカスタマイズ/オーバーライドできます。その主なアイデアは、深いマージだけではありません。2つのキーが同じ場合、値はどうなりますか?このライブラリはそれを処理します— 2つのキーが衝突した場合object-merge-advanced、タイプを比較検討して、マージ後に可能な限り多くのデータを保持することを目指します。

可能な限り多くのデータを保持するためのオブジェクトキーのマージの重み付けキー値タイプ

最初の入力引数のキーは#1、2番目の引数のキーは#2とマークされています。それぞれのタイプに応じて、結果キーの値として1つが選択されます。図では、「オブジェクト」はプレーンオブジェクトを意味します(配列などではない)を。

キーが衝突しない場合、それらはすべて結果を入力します。

サンプルスニペットから、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の選択肢は爆弾である
danday74

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のような巨大なライブラリを必要とせずに1つのライナーを使用したい場合は、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)を使用できます。真のは、2つのオブジェクトを再帰的にマージし、最初のオブジェクトを変更することを示します。

$ extend(true、target、object)


9
質問者は、jqueryを使用していることを決して示しておらず、ネイティブのjavascriptソリューションを求めているようです。
Teh JoE 2017年

これは非常に簡単な方法であり、機能します。私がこの質問をするのが私だった場合に私が検討する実行可能な解決策。:)
kashiraja 2017

これは非常に良い答えですが、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

キャッシュされたredux状態をロードするときにこの問題が発生していました。キャッシュされた状態をロードするだけでは、状態構造が更新された新しいアプリバージョンでエラーが発生します。

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}はマージされません。for()の前にif(current)を追加するだけです。
Andreas Pardeike、

提案をありがとう
Vincent

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関数の素晴らしいライブラリであるRamdaには、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でディープマージを取得するための2行のソリューションしか見つかりませんでした。これがどのように機能するかを教えてください。

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}}})ます。また、たとえばでobj2にも既存の値がある場合、Ilはobj1の既存の値の更新に失敗しmerge({x:{y:1}}, {x:{y:2}})ます。
Oreilles

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;

};

このリンクで質問に答えることができますが、回答の重要な部分をここに含め、参照用のリンクを提供することをお勧めします。リンクされたページが変更されると、リンクのみの回答が無効になる可能性があります。- 口コミより
クリスCamaratta

こんにちは@ChrisCamaratta。ここに不可欠な部分があるだけでなく、すべてがここにあります-関数とその使用方法。したがって、これはリンクのみの回答ではありません。これは、オブジェクトを深くマージするために使用してきた関数です。リンクは、それがどのように機能するかの作者の説明が必要な場合のみです。JavaScriptを教える作者よりも、その仕組みを試し、説明することは、コミュニティにとって害になると思います。コメントをありがとう。
John Shearing

ええと。見逃したか、レビューしたときにコードがレビュー担当者のインターフェースに表示されませんでした。これは質の高い答えであることに同意します。他のレビュアーが私の最初の評価を上書きしたように見えるので、大丈夫だと思います。インスピレーションの旗をごめんなさい。
Chris Camaratta

すごい!@ChrisCamaratta、何が起こったのか理解してくれてありがとう。
John Shearing
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.