llamar - incluir javascript en html5
¿Cómo fusionar profundamente en lugar de fusionar superficialmente? (30)
Tanto Object.assign como Object spread solo hacen una fusión superficial.
Un ejemplo del problema:
// No object nesting
const x = { a: 1 }
const y = { b: 1 }
const z = { ...x, ...y } // { a: 1, b: 1 }
El resultado es lo que esperarías. Sin embargo, si intento esto:
// Object nesting
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = { ...x, ...y } // { a: { b: 1 } }
En lugar de
{ a: { a: 1, b: 1 } }
usted obtiene
{ a: { b: 1 } }
x se sobrescribe por completo porque la sintaxis de propagación solo tiene un nivel de profundidad.
Esto es lo mismo con
Object.assign()
.
¿Hay alguna forma de hacer esto?
¿Alguien sabe si existe una fusión profunda en la especificación ES6 / ES7?
La documentación de Object.assign sugiere que no hace clones profundos.
¿Alguien sabe si existe una fusión profunda en la especificación ES6 / ES7?
No, no lo hace.
¿Hay alguna forma de hacer esto?
Si
las bibliotecas npm
pueden usarse como una solución,
object-merge-advanced
de la suya realmente permite fusionar objetos profundamente y personalizar / anular cada acción de fusión utilizando una función de devolución de llamada familiar.
La idea principal es más que una fusión profunda: ¿qué sucede con el valor cuando dos claves son
iguales
?
Esta biblioteca se encarga de eso: cuando dos claves chocan,
object-merge-advanced
pesa los tipos, con el objetivo de retener la mayor cantidad de datos posible después de la fusión:
La clave del primer argumento de entrada está marcada # 1, el segundo argumento - # 2. Dependiendo de cada tipo, se elige uno para el valor de la clave de resultado. En el diagrama, "un objeto" significa un objeto simple (no una matriz, etc.).
Cuando las teclas no chocan, todas ingresan el resultado.
Desde su fragmento de ejemplo, si utilizó
object-merge-advanced
para fusionar su fragmento de código:
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
// }
// }
Su algoritmo atraviesa recursivamente todas las claves de objetos de entrada, compara y construye y devuelve el nuevo resultado combinado.
A veces no necesita una fusión profunda, incluso si lo cree así. Por ejemplo, si tiene una configuración predeterminada con objetos anidados y desea ampliarla profundamente con su propia configuración, puede crear una clase para eso. El concepto es muy simple:
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,
},
});
});
});
Puede convertirlo en una función (no en un constructor).
Aquí hay otro que acabo de escribir que admite matrices. Los concatena.
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);
};
Esta es una fusión profunda y barata que utiliza el mínimo código posible. Cada fuente sobrescribe la propiedad anterior cuando 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 }));
Hago este método para asignación profunda usando es6.
function isObject(item) {
return (item && typeof item === ''object'' && !Array.isArray(item) && item !== null)
}
function deepAssign(...objs) {
if (objs.length < 2) {
throw new Error(''Need two or more objects to merge'')
}
const target = objs[0]
for (let i = 1; i < objs.length; i++) {
const source = objs[i]
Object.keys(source).forEach(prop => {
const value = source[prop]
if (isObject(value)) {
if (target.hasOwnProperty(prop) && isObject(target[prop])) {
target[prop] = deepAssign(target[prop], value)
} else {
target[prop] = value
}
} else if (Array.isArray(value)) {
if (target.hasOwnProperty(prop) && Array.isArray(target[prop])) {
const targetArray = target[prop]
value.forEach((sourceItem, itemIndex) => {
if (itemIndex < targetArray.length) {
const targetItem = targetArray[itemIndex]
if (Object.is(targetItem, sourceItem)) {
return
}
if (isObject(targetItem) && isObject(sourceItem)) {
targetArray[itemIndex] = deepAssign(targetItem, sourceItem)
} else if (Array.isArray(targetItem) && Array.isArray(sourceItem)) {
targetArray[itemIndex] = deepAssign(targetItem, sourceItem)
} else {
targetArray[itemIndex] = sourceItem
}
} else {
targetArray.push(sourceItem)
}
})
} else {
target[prop] = value
}
} else {
target[prop] = value
}
})
}
return target
}
Hay bibliotecas bien mantenidas que ya hacen esto. Un ejemplo en el registro npm es merge-deep
Ramda, que es una buena biblioteca de funciones de JavaScript, tiene mergeDeepLeft y mergeDeepRight. Cualquiera de estos funciona bastante bien para este problema. Consulte la documentación aquí: https://ramdajs.com/docs/#mergeDeepLeft
Para el ejemplo específico en cuestión podemos usar:
import { mergeDeepLeft } from ''ramda''
const x = { a: { a: 1 } }
const y = { a: { b: 1 } }
const z = mergeDeepLeft(x, y)) // {"a":{"a":1,"b":1}}
Si desea tener una línea única sin requerir una
gran
biblioteca como lodash, le sugiero que use
deepmerge
.
(
npm install deepmerge
)
Entonces puedes hacer
deepmerge({ a: 1, b: 2, c: 3 }, { a: 2, d: 3 });
Llegar
{ a: 2, b: 2, c: 3, d: 3 }
Lo bueno es que viene con tipings para TypeScript de inmediato.
Utiliza esta función:
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;
}
Aquí está la implementación de 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);
};
Y pruebas unitarias:
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*/}
}
Aquí hay otra solución ES6, funciona con objetos y matrices.
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;
}
Aquí hay una versión inmutable (no modifica las entradas) de la respuesta de @ Salakar. Útil si estás haciendo cosas de tipo de programación funcional.
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;
}
Dado que este problema aún está activo, aquí hay otro enfoque:
- ES6 / 2015
- Inmutable (no modifica objetos originales)
- Maneja matrices (las concatena)
/**
* 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);
El paquete deepmerge npm parece ser la biblioteca más utilizada para resolver este problema: https://www.npmjs.com/package/deepmerge
El problema no es trivial cuando se trata de objetos host o cualquier tipo de objeto que sea más complejo que una bolsa de valores
- ¿Invoca a un captador para obtener un valor o copia el descriptor de propiedad?
- ¿Qué pasa si el objetivo de fusión tiene un establecedor (propiedad propia o en su cadena de prototipo)? ¿Considera que el valor ya está presente o llama al configurador para actualizar el valor actual?
- ¿Invoca funciones de propiedad propia o las copia? ¿Qué pasa si son funciones ligadas o funciones de flecha dependiendo de algo en su cadena de alcance en el momento en que se definieron?
- ¿Qué pasa si es algo así como un nodo DOM? Ciertamente no desea tratarlo como un objeto simple y simplemente fusionar todas sus propiedades en
- ¿Cómo lidiar con estructuras "simples" como matrices o mapas o conjuntos? ¿Considerarlos ya presentes o fusionarlos también?
- ¿Cómo lidiar con propiedades propias no enumerables?
- ¿Qué pasa con los nuevos subárboles? ¿Simplemente asignar por referencia o clon profundo?
- ¿Cómo lidiar con objetos congelados / sellados / no extensibles?
Otra cosa a tener en cuenta: gráficos de objetos que contienen ciclos.
Por lo general, no es difícil de tratar, simplemente mantenga un
Set
de objetos de origen ya visitados, pero a menudo se olvida.
Probablemente debería escribir una función de fusión profunda que solo espere valores primitivos y objetos simples, en la mayoría de los tipos que el algoritmo de clonación estructurada puede manejar , como fuentes de fusión. Lanza si encuentra algo que no puede manejar o simplemente asignar por referencia en lugar de una fusión profunda.
En otras palabras, no existe un algoritmo único para todos, ya sea que tenga que usar el suyo propio o buscar un método de biblioteca que cubra sus casos de uso.
Estaba teniendo este problema al cargar un estado redux en caché. Si solo cargara el estado en caché, me encontraría con errores para la nueva versión de la aplicación con una estructura de estado actualizada.
Ya se mencionó que lodash ofrece la función de
merge
, que utilicé:
const currentInitialState = configureState().getState();
const mergedState = _.merge({}, currentInitialState, cachedState);
const store = configureState(mergedState);
La mayoría de los ejemplos aquí parecen demasiado complejos, estoy usando uno en TypeScript que creé, creo que debería cubrir la mayoría de los casos (estoy manejando matrices como datos regulares, solo reemplazándolos).
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;
};
Lo mismo en JS simple, por si acaso:
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
};
};
Aquí están mis casos de prueba para mostrar cómo podría usarlo
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'' } } });
});
});
});
Avíseme si cree que me falta alguna funcionalidad.
La siguiente función realiza una copia profunda de los objetos, abarca la copia de primitivas, matrices y objetos.
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
}
Me gustaría presentar una alternativa ES5 bastante simple.
La función obtiene 2 parámetros:
target
y
source
que deben ser de tipo "objeto".
Target
será el objeto resultante.
Target
mantiene todas sus propiedades originales, pero sus valores pueden modificarse.
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;
}
casos:
-
si el
target
no tiene una propiedadsource
, eltarget
obtiene; -
si el
target
tiene una propiedadsource
y eltarget
y lasource
no son ambos objetos (3 de 4 casos), la propiedad deltarget
se anula; -
si el
target
tiene una propiedadsource
y ambos son objetos / matrices (1 caso restante), entonces la recursión ocurre al fusionar dos objetos (o concatenación de dos matrices);
También considere lo siguiente :
- array + obj = array
- obj + array = obj
- obj + obj = obj (fusión recursiva)
- matriz + matriz = matriz (concat)
Es predecible, admite tipos primitivos, así como matrices y objetos. Además, como podemos fusionar 2 objetos, creo que podemos fusionar más de 2 mediante reduce función de reduce .
Eche un vistazo a un ejemplo (y juegue con él si lo desea) :
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));
Hay una limitación: la longitud de la pila de llamadas del navegador. Los navegadores modernos arrojarán un error en un nivel de recursión realmente profundo (piense en miles de llamadas anidadas). También es libre de tratar situaciones como matriz + objeto, etc., como desee agregando nuevas condiciones y comprobaciones de tipo.
Podemos usar $ .extend (true, object1, object2) para una fusión profunda. Valor verdadero denota fusionar dos objetos de forma recursiva, modificando el primero.
Puede usar la fusión de 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 }] }
Sé que este es un problema un poco antiguo, pero la solución más fácil en ES2015 / ES6 que pude encontrar fue en realidad bastante simple, usando Object.assign (),
Esperemos que esto ayude:
/**
* 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);
}
Ejemplo de uso:
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] } } }
Encontrarás una versión inmutable de esto en la respuesta a continuación.
Tenga en cuenta que esto conducirá a una recursión infinita en referencias circulares. Aquí hay algunas respuestas excelentes sobre cómo detectar referencias circulares si crees que enfrentarías este problema.
Sé que ya hay muchas respuestas y tantos comentarios argumentando que no funcionarán. El único consenso es que es tan complicado que nadie hizo un estándar para ello . Sin embargo, la mayoría de las respuestas aceptadas en SO exponen "trucos simples" que son ampliamente utilizados. Por lo tanto, para todos nosotros, como yo, que no somos expertos pero queremos escribir código más seguro al comprender un poco más sobre la complejidad de JavaScript, intentaré arrojar algo de luz.
Antes de ensuciarnos las manos, déjenme aclarar 2 puntos:
- [DESCARGO DE RESPONSABILIDAD] Propongo una función a continuación que aborda cómo hacemos un bucle profundo en los objetos de JavaScript para copiar e ilustra lo que generalmente se comenta demasiado brevemente. No está listo para la producción. En aras de la claridad, he dejado de lado deliberadamente otras consideraciones como objetos circulares (seguimiento por un conjunto o propiedad de símbolo no conflictiva) , copia del valor de referencia o clon profundo , objeto de destino inmutable (¿clon profundo nuevamente?), Estudio caso por caso de cada tipo de objetos , obtener / establecer propiedades a través de los accessors ... Además, no probé el rendimiento, aunque es importante, porque tampoco es el punto aquí.
-
Usaré
copiar
o
asignar
términos en lugar de
fusionar
.
Porque en mi opinión, una
fusión
es conservadora y debería fallar en los conflictos.
Aquí, cuando entra en conflicto, queremos que la fuente sobrescriba el destino.
Como hace
Object.assign
.
Las respuestas con
for..in
u
Object.keys
son engañosas
Hacer una copia profunda parece una práctica tan básica y común que esperamos encontrar una línea o, al menos, una victoria rápida a través de una recursión simple. No esperamos que necesitemos una biblioteca o escribir una función personalizada de 100 líneas.
Cuando leí por primera
vez la respuesta de Salakar
, realmente pensé que podría hacerlo mejor y más simple (puede compararlo con
Object.assign
en
x={a:1}, y={a:{b:1}}
).
Luego leí
la respuesta del 8472
y pensé ... no hay
escapatoria
tan fácil, mejorar las respuestas ya dadas no nos llevará lejos.
Dejemos una copia profunda y recursiva a un lado al instante. Solo considere cómo (erróneamente) las personas analizan las propiedades para copiar un objeto muy 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
omitirá sus propias propiedades no enumerables, sus propias propiedades con clave de símbolo y todas las propiedades del prototipo.
Puede estar bien si tus objetos no tienen ninguno de esos.
Pero tenga en cuenta que
Object.assign
maneja sus propias propiedades enumerables con clave de símbolo.
Entonces su copia personalizada perdió su floración.
for..in
proporcionará propiedades de la fuente, de su prototipo y de la cadena completa del prototipo sin que lo desee (o lo sepa).
Su objetivo puede terminar con demasiadas propiedades, mezclando propiedades prototipo y propiedades propias.
Si está escribiendo una función de propósito general y no está utilizando
Object.getOwnPropertyDescriptors
,
Object.getOwnPropertyNames
,
Object.getOwnPropertySymbols
u
Object.getPrototypeOf
, lo más probable es que lo esté haciendo mal.
Cosas a considerar antes de escribir su función
Primero, asegúrese de comprender qué es un objeto Javascript. En Javascript, un objeto está hecho de sus propias propiedades y un objeto prototipo (padre). El objeto prototipo a su vez está hecho de sus propias propiedades y un objeto prototipo. Y así sucesivamente, definiendo una cadena prototipo.
Una propiedad es un par de clave (
string
o
symbol
) y descriptor (
value
o
get
/
set
accessor, y atributos como
enumerable
).
Finalmente, hay muchos tipos de objetos . Es posible que desee manejar de manera diferente un objeto Objeto de un objeto Fecha o una función de objeto.
Entonces, al escribir su copia profunda, debe responder al menos esas preguntas:
- ¿Qué considero profundo (adecuado para la búsqueda recursiva) o plano?
- ¿Qué propiedades quiero copiar? (enumerable / no enumerable, con clave de cadena / con clave de símbolo, propiedades propias / propiedades propias del prototipo, valores / descriptores ...)
Para mi ejemplo, considero que solo los
object Object
s son
profundos
, porque otros objetos creados por otros constructores pueden no ser adecuados para una mirada en profundidad.
Personalizado a partir de
este 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);
}
E hice un objeto de
options
para elegir qué copiar (para fines de demostración).
const options = {nonEnum:true, symbols:true, descriptors: true, proto:true};
Función propuesta
Puedes probarlo en este 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;
}
}
Eso se puede usar así:
const x = { a: { a: 1 } },
y = { a: { b: 1 } };
deepAssign(options)(x,y); // { a: { a: 1, b: 1 } }
Si está usando
ImmutableJS
, puede usar
mergeDeep
:
fromJS(options).mergeDeep(options2).toJS();
Una solución simple con ES5 (sobrescribir el valor existente):
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));
Yo uso lodash:
import _ = require(''lodash'');
value = _.merge(value1, value2);
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);
Prueba de unidad:
// 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;
}
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]