Object.assign et Object spread ne font qu'une fusion superficielle.
Un exemple du problème:
// No object nesting
const x = { a: 1 }
const y = { b: 1 }
const z = { ...x, ...y } // { a: 1, b: 1 }
La sortie est ce que vous attendez. Cependant si j'essaye ceci:
// Object nesting
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = { ...x, ...y } // { a: { b: 1 } }
Au lieu de
{ a: { a: 1, b: 1 } }
vous obtenez
{ a: { b: 1 } }
x est complètement écrasé car la syntaxe de propagation ne va qu'à un niveau. C'est la même chose avec Object.assign()
.
Y a-t-il un moyen de faire cela?
Est-ce que quelqu'un sait s'il existe une fusion profonde dans la spécification ES6/ES7?
Non.
Je sais que c’est un peu un vieux problème, mais la solution la plus simple que j’ai pu trouver dans ES2015/ES6 était en fait assez simple, en utilisant Object.assign (),
Espérons que cela aide:
/**
* 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);
}
Exemple d'utilisation:
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] } } }
Vous trouverez une version immuable de cela dans la réponse ci-dessous.
Notez que cela conduira à une récursion infinie sur les références circulaires. Il existe d'excellentes réponses sur la façon de détecter les références circulaires si vous pensez faire face à ce problème.
Le problème n'est pas trivial lorsqu'il s'agit d'objets Host ou de tout type d'objet plus complexe qu'un sac de valeurs
Une autre chose à garder à l’esprit: les graphiques d’objets contenant des cycles. Ce n’est généralement pas difficile à gérer - conservez simplement une Set
d’objets source déjà visités - mais souvent oubliée.
Vous devriez probablement écrire une fonction de fusion profonde qui n'attend que des valeurs primitives et des objets simples - tout au plus les types que l'algorithme de clone structuré peut gérer - en tant que sources de fusion. Jetez-la si elle rencontre un problème qu’elle ne peut pas gérer ou simplement attribuer par référence au lieu d’une fusion en profondeur.
En d'autres termes, il n'y a pas d'algorithme unique, vous devez soit rouler le vôtre, soit rechercher une méthode de bibliothèque qui couvre vos cas d'utilisation.
Vous pouvez utiliser fusion 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 }] }
Voici une version immuable (ne modifie pas les entrées) de la réponse de @ Salakar. Utile si vous faites des choses de type programmation fonctionnelle.
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;
}
Puisque ce problème est toujours actif, voici une autre approche:
/**
* 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);
Je sais qu'il y a déjà beaucoup de réponses et autant de commentaires prétendant qu'elles ne fonctionneront pas. Le seul consensus est que , c'est tellement compliqué que personne n'a fait de standard pour cela . Cependant, la plupart des réponses acceptées dans SO exposent des "astuces simples" largement utilisées. Donc, pour nous tous comme moi qui ne sommes pas des experts mais qui veulent écrire du code plus sûr en apprenant un peu plus sur la complexité de javascript, je vais essayer de vous éclairer un peu.
Avant de nous salir les mains, laissez-moi clarifier deux points:
Object.assign
le fait.for..in
ou Object.keys
sont trompeusesFaire une copie en profondeur semble une pratique si élémentaire et si courante que nous nous attendons à trouver une solution unique ou, du moins, une victoire rapide via une simple récursion. Nous ne pensons pas avoir besoin d'une bibliothèque ou écrire une fonction personnalisée de 100 lignes.
Quand j'ai lu pour la première fois réponse de Salakar , j'ai vraiment pensé que je pouvais faire mieux et plus simplement (vous pouvez le comparer avec Object.assign
sur x={a:1}, y={a:{b:1}}
). Ensuite, j'ai lu réponse de the8472 et je me suis dit ... on ne peut pas s'éloigner si facilement, l'amélioration des réponses déjà données ne nous mènera pas loin.
Laissons les copies profondes et récursives de côté un instant. Il suffit d’examiner comment les personnes analysent (à tort) les propriétés pour copier un objet très simple.
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
omettra ses propres propriétés non énumérables, ses propres propriétés associées à des symboles et toutes les propriétés du prototype. Cela peut aller si vos objets n'en ont pas. Mais gardez à l'esprit que Object.assign
gère ses propres propriétés énumérables à symbole. Donc, votre copie personnalisée a perdu sa floraison.
for..in
fournira les propriétés de la source, de son prototype et de la chaîne de prototypes complète sans que vous ne le vouliez (ou le sachiez). Votre cible risque de se retrouver avec trop de propriétés, mélangeant propriétés de prototype et propriétés propres.
Si vous écrivez une fonction d'usage général et que vous n'utilisez pas Object.getOwnPropertyDescriptors
, Object.getOwnPropertyNames
, Object.getOwnPropertySymbols
ou Object.getPrototypeOf
, vous le faites probablement mal.
Tout d'abord, assurez-vous de bien comprendre ce qu'est un objet Javascript. En Javascript, un objet est constitué de ses propres propriétés et d'un objet prototype (parent). L'objet prototype est à son tour constitué de ses propres propriétés et d'un objet prototype. Et ainsi de suite, définissant une chaîne de prototypes.
Une propriété est une paire de clés (string
ou symbol
) et d'un descripteur (value
ou get
/set
accesseur et d'attributs tels que enumerable
).
Enfin, il y a plusieurs types d'objets . Vous souhaiterez peut-être gérer différemment un objet Objet à partir d'un objet Date ou une fonction objet.
Donc, en écrivant votre copie conforme, vous devriez au moins répondre aux questions suivantes:
Pour mon exemple, j'estime que seuls les object Object
s sont profond , car d'autres objets créés par d'autres constructeurs risquent de ne pas convenir à un examen approfondi. Personnalisé à partir de 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);
}
Et j’ai créé un objet options
pour choisir le contenu à copier (à des fins de démonstration).
const options = {nonEnum:true, symbols:true, descriptors: true, proto:true};
Vous pouvez le tester dans ce plunker .
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;
}
}
Cela peut être utilisé comme ceci:
const x = { a: { a: 1 } },
y = { a: { b: 1 } };
deepAssign(options)(x,y); // { a: { a: 1, b: 1 } }
J'utilise lodash:
import _ = require('lodash');
value = _.merge(value1, value2);
Le paquet deepmerge npm semble être la bibliothèque la plus utilisée pour résoudre ce problème: https://www.npmjs.com/package/deepmerge
Voici l'implémentation 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);
};
Et tests unitaires:
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*/}
}
Je voudrais présenter une alternative assez simple ES5. La fonction obtient 2 paramètres - target
et source
qui doivent être de type "objet". Target
sera l'objet résultant. Target
conserve toutes ses propriétés d'origine mais leurs valeurs peuvent être modifiées.
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;
}
cas:
target
n'a pas de propriété source
, target
l'obtient;target
a une propriété source
et target
& source
ne sont pas les deux objets (3 cas sur 4), la propriété de target
est annulée;target
possède une propriété source
et que tous deux sont des objets/des tableaux (il reste 1 cas), la récursivité se produit alors fusionnant deux objets (ou la concaténation de deux tableaux);considérons également les éléments suivants :
Il est prévisible, prend en charge les types primitifs ainsi que les tableaux et les objets. Aussi, comme nous pouvons fusionner 2 objets, je pense que nous pouvons fusionner plus de 2 via la fonction réduire .
jetez un coup d'œil à un exemple (et faites-le jouer si vous le souhaitez) :
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));
Il existe une limitation - la longueur de la pile d'appels du navigateur. Les navigateurs modernes émettront une erreur à un niveau de récursion très profond (pensez à des milliers d'appels imbriqués). De plus, vous êtes libre de traiter les situations telles que tableau + objet, etc., en ajoutant de nouvelles conditions et des contrôles de type.
Si vous utilisez ImmutableJS , vous pouvez utiliser mergeDeep
:
fromJS(options).mergeDeep(options2).toJS();
Nous pouvons utiliser $ .Extend (true, object1, object2) pour une fusion profonde. La valeur true indique de fusionner deux objets de manière récursive en modifiant le premier.
Voici une autre solution ES6 qui fonctionne avec des objets et des tableaux.
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
}
La fonction suivante crée une copie profonde d’objets, elle couvre la copie primitive, les tableaux ainsi que les objets.
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;
}
Y a-t-il un moyen de faire cela?
Si les bibliothèques npm peuvent être utilisées comme solution, object-merge-advanced de votre part permet vraiment de fusionner des objets en profondeur et de les personnaliser/substitue chaque action de fusion en utilisant une fonction de rappel familière. L'idée principale est plus qu'une simple fusion en profondeur - qu'advient-il de la valeur lorsque deux clés sont identiques ? Cette bibliothèque s’occupe de cela - lorsque deux clés s’entrechoquent, object-merge-advanced
pèse les types, dans le but de conserver autant de données que possible après la fusion:
La clé du premier argument d'entrée est marquée # 1, celle du second argument - # 2. En fonction de chaque type, une valeur est choisie pour la clé de résultat. Dans le diagramme, "un objet" signifie un objet simple (pas un tableau, etc.).
Lorsque les touches ne sont pas en conflit, elles entrent toutes dans le résultat.
À partir de votre exemple d’extrait de code, si vous avez utilisé object-merge-advanced
pour fusionner votre extrait de code:
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
// }
// }
Son algorithme parcourt de manière récursive toutes les clés d'objet d'entrée, compare et construit et renvoie le nouveau résultat fusionné.
Une solution simple avec ES5 (écraser la valeur existante):
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));
J'avais ce problème lors du chargement d'un état de redux en cache. Si je ne fais que charger l'état en cache, je rencontrerais des erreurs pour la nouvelle version de l'application avec une structure d'état mise à jour.
Il a déjà été mentionné que lodash offre la fonction merge
que j'ai utilisée:
const currentInitialState = configureState().getState();
const mergedState = _.merge({}, currentInitialState, cachedState);
const store = configureState(mergedState);
La plupart des exemples ici semblent trop complexes. J'en utilise un dans TypeScript que j'ai créé. Je pense que cela devrait couvrir la plupart des cas (je traite les tableaux comme des données ordinaires, en les remplaçant).
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;
};
Même chose en clair JS, juste au cas où:
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
};
};
Voici mes cas de test pour montrer comment vous pouvez l'utiliser
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' } } });
});
});
});
Faites-moi savoir s'il vous manque des fonctionnalités.
Voici un autre que je viens d'écrire qui supporte les tableaux. Cela les concats.
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);
};
Ramda qui est une belle bibliothèque de fonctions javascript a mergeDeepLeft et mergeDeepRight. N'importe lequel de ces fonctionne assez bien pour ce problème. Veuillez consulter la documentation ici: https://ramdajs.com/docs/#mergeDeepLeft
Pour l'exemple spécifique en question, nous pouvons utiliser:
import { mergeDeepLeft } from 'ramda'
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = mergeDeepLeft(x, y)) // {"a":{"a":1,"b":1}}
Utilisez cette fonction:
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;
}
Si vous voulez avoir une seule ligne sans nécessiter une énorme bibliothèque comme lodash, je vous suggère d'utiliser deepmerge . (npm install deepmerge
)
Ensuite, vous pouvez faire
deepmerge({ a: 1, b: 2, c: 3 }, { a: 2, d: 3 });
obtenir
{ a: 2, b: 2, c: 3, d: 3 }
La bonne chose est qu’il est livré avec des typages pour TypeScript tout de suite.
// 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;
}
Test de l'unité:
describe('recursivelyMoveProperties', () => {
it('should copy properties correctly', () => {
const source: any = {
propS1: 'str1',
propS2: 'str2',
propN1: 1,
propN2: 2,
propA1: [1, 2, 3],
propA2: [],
propB1: true,
propB2: false,
propU1: null,
propU2: null,
propD1: undefined,
propD2: undefined,
propO1: {
subS1: 'sub11',
subS2: 'sub12',
subN1: 11,
subN2: 12,
subA1: [11, 12, 13],
subA2: [],
subB1: false,
subB2: true,
subU1: null,
subU2: null,
subD1: undefined,
subD2: undefined,
},
propO2: {
subS1: 'sub21',
subS2: 'sub22',
subN1: 21,
subN2: 22,
subA1: [21, 22, 23],
subA2: [],
subB1: false,
subB2: true,
subU1: null,
subU2: null,
subD1: undefined,
subD2: undefined,
},
};
let dest: any = {
propS2: 'str2',
propS3: 'str3',
propN2: -2,
propN3: 3,
propA2: [2, 2],
propA3: [3, 2, 1],
propB2: true,
propB3: false,
propU2: 'not null',
propU3: null,
propD2: 'defined',
propD3: undefined,
propO2: {
subS2: 'inv22',
subS3: 'sub23',
subN2: -22,
subN3: 23,
subA2: [5, 5, 5],
subA3: [31, 32, 33],
subB2: false,
subB3: true,
subU2: 'not null --- ',
subU3: null,
subD2: ' not undefined ----',
subD3: undefined,
},
propO3: {
subS1: 'sub31',
subS2: 'sub32',
subN1: 31,
subN2: 32,
subA1: [31, 32, 33],
subA2: [],
subB1: false,
subB2: true,
subU1: null,
subU2: null,
subD1: undefined,
subD2: undefined,
},
};
dest = recursivelyMoveProperties(source, dest);
expect(dest).toEqual({
propS1: 'str1',
propS2: 'str2',
propS3: 'str3',
propN1: 1,
propN2: 2,
propN3: 3,
propA1: [1, 2, 3],
propA2: [],
propA3: [3, 2, 1],
propB1: true,
propB2: false,
propB3: false,
propU1: null,
propU2: null,
propU3: null,
propD1: undefined,
propD2: undefined,
propD3: undefined,
propO1: {
subS1: 'sub11',
subS2: 'sub12',
subN1: 11,
subN2: 12,
subA1: [11, 12, 13],
subA2: [],
subB1: false,
subB2: true,
subU1: null,
subU2: null,
subD1: undefined,
subD2: undefined,
},
propO2: {
subS1: 'sub21',
subS2: 'sub22',
subS3: 'sub23',
subN1: 21,
subN2: 22,
subN3: 23,
subA1: [21, 22, 23],
subA2: [],
subA3: [31, 32, 33],
subB1: false,
subB2: true,
subB3: true,
subU1: null,
subU2: null,
subU3: null,
subD1: undefined,
subD2: undefined,
subD3: undefined,
},
propO3: {
subS1: 'sub31',
subS2: 'sub32',
subN1: 31,
subN2: 32,
subA1: [31, 32, 33],
subA2: [],
subB1: false,
subB2: true,
subU1: null,
subU2: null,
subD1: undefined,
subD2: undefined,
},
});
});
});
Parfois, vous n'avez pas besoin d'une fusion profonde, même si vous le pensez. Par exemple, si vous avez une configuration par défaut avec des objets imbriqués et que vous souhaitez l'étendre profondément avec votre propre configuration, vous pouvez créer une classe pour cela. Le concept est très simple:
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);
Vous pouvez le convertir en une fonction (pas un constructeur).
Est-ce que quelqu'un sait s'il existe une fusion profonde dans la spécification ES6/ES7?
documentation Object.assign suggère qu'il ne fait pas de clonage en profondeur.
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]
Il existe déjà des bibliothèques bien gérées. Un exemple sur le registre npm est merge-deep
Il s'agit d'une fusion en profondeur peu coûteuse qui utilise le moins de code possible. Chaque source écrase la propriété précédente lorsqu'elle existe.
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 }));