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はスプレッド構文が1レベルだけ深くなるので完全に上書きされます。これはObject.assign()
と同じです。
これを行う方法はありますか?
ES6/ES7仕様にディープマージがあるかどうか誰かが知っていますか?
いいえ、違います。
これはちょっと古い問題ですが、私が思いつくことができる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] } } }
あなたは以下の答えでこれの不変のバージョンを見つけるでしょう。
循環参照では無限再帰につながることに注意してください。あなたがこの問題に直面していると思うなら、循環参照を検出する方法について ここにいくつかの素晴らしい答えがあります。
Hostオブジェクトや値の集まりよりも複雑なあらゆる種類のオブジェクトに関しては、問題は簡単ではありません。
覚えておくべきもう一つのこと:サイクルを含むオブジェクトグラフ。通常、処理することは難しくありません。単に訪問済みのソースオブジェクトのSet
を保持するだけですが、忘れられがちです。
プリミティブな値と単純なオブジェクト(最大でも 構造化クローンアルゴリズムが - をマージソースとして扱うことができるもの)だけを期待するディープマージ関数を書くべきでしょう。処理できないものに遭遇した場合、またはディープマージの代わりに単に参照で代入した場合にスローします。
言い換えれば、万能のアルゴリズムはありません、あなた自身のものを転がすか、あなたのユースケースをカバーするために起こるライブラリメソッドを探す必要があります。
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 }] }
これは@ 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;
}
この問題はまだ活発であるので、これは別のアプローチです:
/**
* 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);
私はすでにたくさんの答えがあることを知っていますし、それらが機能しないと主張する多くのコメントがあります。唯一のコンセンサスは、誰も標準にしていないのでとても複雑ですです。ただし、SOで受け入れられている回答のほとんどは、広く使用されている「単純なトリック」を公開しています。それで、私のように専門家ではないが、JavaScriptの複雑さについてもう少し理解してより安全なコードを書きたいと思う私たちみんなにとって、私はいくらか光を当てるつもりです。
手を汚す前に、2点を明確にしましょう。
Object.assign
のように。for..in
またはObject.keys
による回答は誤解を招くディープコピーを作成することは非常に基本的かつ一般的なやり方であるように思われるので、ワンライナーを見つけること、または少なくとも単純な再帰による簡単な勝利を見つけることを期待しています。ライブラリを必要としたり、100行のカスタム関数を書いたりする必要はないでしょう。
最初に Salakarの答え を読んだとき、私は本当にもっと簡単でもっとできると思いました(Object.assign
のx={a:1}, y={a:{b:1}}
と比較できます)。それから私は the 8472の答え そして私は思った...それほど簡単に逃げることはできません。すでに与えられた答えを改善しても私たちはそれを達成できません。
ディープコピーと再帰的な処理をさせておきましょう。非常に単純なオブジェクトをコピーするために(誤って)人々がどのようにプロパティを解析するかを考えてください。
const y = Object.create(
{ proto : 1 },
{ a: { enumerable: true, value: 1},
[Symbol('b')] : { enumerable: true, value: 1} } )
Object.assign({},y)
> { 'a': 1, Symbol(b): 1 } // All (enumerable) properties are copied
((x,y) => Object.keys(y).reduce((acc,k) => Object.assign(acc, { [k]: y[k] }), x))({},y)
> { 'a': 1 } // Missing a property!
((x,y) => {for (let k in y) x[k]=y[k];return x})({},y)
> { 'a': 1, 'proto': 1 } // Missing a property! Prototype's property is copied too!
Object.keys
は、列挙不可能な独自のプロパティ、シンボルキー付きの独自のプロパティ、およびすべてのプロトタイプのプロパティを省略します。あなたのオブジェクトがそれらのどれも持っていないのであれば、それは良いかもしれません。ただし、Object.assign
は独自のシンボルキー列挙型プロパティを処理することに注意してください。それであなたのカスタムコピーはその開花を失いました。
for..in
は、あなたがそれを望まずに(あるいはそれを知らなくても)、ソース、そのプロトタイプ、そして完全なプロトタイプチェーンのプロパティを提供します。あなたのターゲットは、プロトタイプのプロパティと独自のプロパティを混同して、あまりにも多くのプロパティになってしまうかもしれません。
あなたが汎用目的の関数を書いていて、あなたがObject.getOwnPropertyDescriptors
、Object.getOwnPropertyNames
、Object.getOwnPropertySymbols
またはObject.getPrototypeOf
を使っていないならば、あなたはおそらくそれを間違ってやっています。
まず、Javascriptオブジェクトとは何かを理解してください。 Javascriptでは、オブジェクトはそれ自身のプロパティと(親)プロトタイプオブジェクトでできています。プロトタイプオブジェクトは、それ自体のプロパティとプロトタイプオブジェクトで構成されています。そして、プロトタイプチェーンを定義します。
プロパティは、キー(string
またはsymbol
)と記述子(value
またはget
/set
のアクセサ、およびenumerable
のような属性)のペアです。
たくさんの種類のオブジェクトがあります 。オブジェクトDateをオブジェクトDateまたはオブジェクトFunctionとは異なる方法で処理することができます。
だから、あなたの深いコピーを書いて、あなたは少なくともこれらの質問に答えるべきです:
私の例では、object Object
sだけがdeepであると考えます。他のコンストラクタによって作成された他のオブジェクトは詳細な外観には適していない可能性があるためです。 this 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 } }
これがTypeScriptの実装です。
export const mergeObjects = <T extends object = object>(target: T, ...sources: T[]): T => {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (source === undefined) {
return target;
}
if (isMergebleObject(target) && isMergebleObject(source)) {
Object.keys(source).forEach(function(key: string) {
if (isMergebleObject(source[key])) {
if (!target[key]) {
target[key] = {};
}
mergeObjects(target[key], source[key]);
} else {
target[key] = source[key];
}
});
}
return mergeObjects(target, ...sources);
};
const isObject = (item: any): boolean => {
return item !== null && typeof item === 'object';
};
const isMergebleObject = (item): boolean => {
return isObject(item) && !Array.isArray(item);
};
そしてユニットテスト:
describe('merge', () => {
it('should merge Objects and all nested Ones', () => {
const obj1 = { a: { a1: 'A1'}, c: 'C', d: {} };
const obj2 = { a: { a2: 'A2'}, b: { b1: 'B1'}, d: null };
const obj3 = { a: { a1: 'A1', a2: 'A2'}, b: { b1: 'B1'}, c: 'C', d: null};
expect(mergeObjects({}, obj1, obj2)).toEqual(obj3);
});
it('should behave like Object.assign on the top level', () => {
const obj1 = { a: { a1: 'A1'}, c: 'C'};
const obj2 = { a: undefined, b: { b1: 'B1'}};
expect(mergeObjects({}, obj1, obj2)).toEqual(Object.assign({}, obj1, obj2));
});
it('should not merge array values, just override', () => {
const obj1 = {a: ['A', 'B']};
const obj2 = {a: ['C'], b: ['D']};
expect(mergeObjects({}, obj1, obj2)).toEqual({a: ['C'], b: ['D']});
});
it('typed merge', () => {
expect(mergeObjects<TestPosition>(new TestPosition(0, 0), new TestPosition(1, 1)))
.toEqual(new TestPosition(1, 1));
});
});
class TestPosition {
constructor(public x: number = 0, public y: number = 0) {/*empty*/}
}
ImmutableJS を使用している場合は、mergeDeep
を使用できます。
fromJS(options).mergeDeep(options2).toJS();
これはES6のもう1つの解決策で、オブジェクトと配列を処理します。
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
}
非常に単純なES5の代替案を提示したいと思います。この関数は2つのパラメータtarget
とsource
を取得します。これらは "object"型でなければなりません。 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
が両方ともオブジェクトではない場合(4つのうち3つのケース)、target
のプロパティはオーバーライドされます。target
がsource
プロパティを持ち、それらが両方ともオブジェクト/配列である場合(残りの1つのケース)、2つのオブジェクトのマージ(または2つの配列の連結)で再帰が発生します。以下も考慮してください :
それは予測可能で、配列とオブジェクトと同様にプリミティブ型をサポートします。また、2つのオブジェクトをマージできるので、 reduce 関数を使って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));
ブラウザの呼び出しスタック長という制限があります。最近のブラウザでは、非常に深い再帰レベルでエラーが発生します(何千ものネストされた呼び出しを考えてください)。また、新しい条件や型チェックを追加することで、配列+オブジェクトなどの状況を自由に扱うことができます。
私はlodashを使います:
import _ = require('lodash');
value = _.merge(value1, value2);
Deepmerge npmパッケージは、この問題を解決するための最も広く使用されているライブラリのようです: https://www.npmjs.com/package/deepmerge
これを行う方法はありますか?
npmライブラリ を解決策として使用できる場合は、 object-merge-advanced を使用すると、本当にオブジェクトを深くマージし、よく知られているコールバック関数を使用してすべてのマージアクションをカスタマイズ/上書きできます。その主な考え方は、単なるディープマージではありません。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
// }
// }
このアルゴリズムは、すべての入力オブジェクトキーを再帰的にトラバースし、新しいマージ結果を比較および構築して返します。
次の関数はオブジェクトのディープコピーを作成し、それはオブジェクトと同様にプリミティブ、配列のコピーをカバーします
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;
}
キャッシュされたredux状態をロードするときにこの問題が発生していました。キャッシュされた状態を単にロードしただけでは、更新された状態構造で新しいアプリバージョンのエラーが発生します。
Lodashが私が使用したmerge
関数を提供することはすでに述べました。
const currentInitialState = configureState().getState();
const mergedState = _.merge({}, currentInitialState, cachedState);
const store = configureState(mergedState);
ディープマージには $ .extend(true、object1、object2) を使用できます。値 true は2つのオブジェクトを再帰的にマージし、最初のオブジェクトを変更することを示します。
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));
これは私が先ほど書いた、配列をサポートするものです。それはそれらを連結します。
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);
};
この機能を使う:
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;
}
Lodashのようなhugeライブラリを必要とせずに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のタイピングがすぐに来ることです。
たとえそうだとしても、時々、あなたはディープマージを必要としません。たとえば、ネストしたオブジェクトを含むデフォルトの設定があり、それを自分の設定で深く拡張したい場合は、そのためのクラスを作成できます。その概念はとても単純です。
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);
あなたはそれを(コンストラクタではなく)関数に変換することができます。
ここでのほとんどの例は複雑すぎるように思われます、私が作成した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' } } });
});
});
});
機能が不足していると思われる場合はお知らせください。
ES6/ES7仕様にディープマージがあるかどうか誰かが知っていますか?
Object.assignドキュメント はディープクローンをしないことを示唆しています。
// 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,
},
});
});
});
JavaScript関数のNiceライブラリである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}}
function isObject(obj) {
return obj !== null && typeof obj === 'object';
}
const isArray = Array.isArray;
function isPlainObject(obj) {
return isObject(obj) && (
obj.constructor === Object // obj = {}
|| obj.constructor === undefined // obj = Object.create(null)
);
}
function mergeDeep(target, ...sources){
if (!sources.length) return target;
const source = sources.shift();
if (isPlainObject(source) || isArray(source)) {
for (const key in source) {
if (isPlainObject(source[key]) || isArray(source[key])) {
if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
target[key] = {};
}else if (isArray(source[key]) && !isArray(target[key])) {
target[key] = [];
}
mergeDeep(target[key], source[key]);
} else if (source[key] !== undefined && source[key] !== '') {
target[key] = source[key];
}
}
}
return mergeDeep(target, ...sources);
}
// test...
var source = {b:333};
var source2 = {c:32, arr: [33,11]}
var n = mergeDeep({a:33}, source, source2);
source2.arr[1] = 22;
console.log(n.arr); // out: [33, 11]
すでにこれを行っているよく整備されたライブラリがあります。 npmレジストリの一例は merge-deep です。
これは私が考えることができるようにできるだけ少ないコードを使用する安価なディープマージです。存在する場合、各ソースは前のプロパティを上書きします。
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 }));