'Recursively update nested object values for specific keys based on another object
*** UPDATED object structure ***
I'd like to update recursively the property values of mainObject from the properties that exist in updatingObject.
let mainObject = {
age: 24,
isMarried: false,
pets: {
dog: {
name: "Juniper",
age: 3
},
cat: {
name: "Spasia",
age: 7
}
},
hobbies: {
mountainRelated: ["hiking", "snowboarding"]
}
}
let updatingObject = {
pets: {
cat: {
age: 8
}
}
}
I've added a Codepen link to the problem below: what I still have to do is to find the correct properties to be updated (for example the "age" property is common for more objects).
TL;DR: cat age should be 8 in the mainObject
Solution 1:[1]
You can traverse in sync
let mainObject = {
age: 24,
isMarried: false,
pets: {
dog: { name: "Juniper", age: 3 },
cat: { name: "Spasia", age: 7 }
},
hobbies: {
mountainRelated: ["hiking", "snowboarding"]
}
}
let updatingObject = {
pets: {
cat: {
age: 8,
name:'gabriele'
}
}
}
function updateObject(target, update){
// for each key/value pair in update object
for (const [key,value] of Object.entries(update)){
// if target has the relevant key and
// the type in target and update is the same
if (target.hasOwnProperty(key) && typeof(value) === typeof(target[key])){
// update value if string,number or boolean
if (['string','number','boolean'].includes(typeof value) || Array.isArray(value)){
target[key] = value;
} else {
// if type is object then go one level deeper
if (typeof value === 'object'){
updateObject(target[key], value)
}
}
}
}
}
updateObject(mainObject,updatingObject)
console.log(mainObject);
Solution 2:[2]
We can write a function that does this without mutating the original object, in a fairly simple manner. This merge
function breaks the original and update objects into key-value pairs, then keeps all those whose keys are only in one of them, and, for those in both, if both values are objects recursively calling merge
on them, otherwise choosing the update value. The resulting key-value pairs are then put back into an object.
Here is an implementation:
const merge = (a, b) => Object .fromEntries ([
... Object .entries (a) .filter (([k]) => !(k in b)),
... Object .entries (b) .filter (([k]) => !(k in a)),
... Object. entries (b) .filter (([k]) => (k in a)) .map (([k, v]) =>
[k, Object (v) === v && Object (a [k]) === a [k] ? merge (a [k], v) : v]
),
])
const mainObject = {age: 24, isMarried: false, pets: {dog: { name: "Juniper", age: 3 }, cat: { name: "Spasia", age: 7 }}, hobbies: {mountainRelated: ["hiking", "snowboarding"]}}
const updatingObject = {pets: {cat: {age: 8, name:'gabriele'}}}
console .log (merge (mainObject, updatingObject))
.as-console-wrapper {max-height: 100% !important; top: 0}
Note that while the resulting object is a new one, we use a form of structural sharing between this and the input objects. For instance, if you push 'hangliding'
onto the array hobbies.mountainRelated
in the resulting object, we will update mainObject
values as well. While we could change this behavior, it helps reduce memory consumption, so I wouldn't do so without good reason.
Note that this does not try to deal with more complicated scenarios, such as cyclic objects. It also does not do anything to work with arrays. Arrays add numerous complexities, and if you need them, I would suggest looking at the equivalent functions in a library such as Ramda's mergeDeepRight
(disclaimer: I'm an author) or lodash's merge
, or dedicated tools like deepmerge
.
Solution 3:[3]
The below approach solves the problem by first constructing the access path to the property of an object, then access and modify the mainObject
according to the access path constructed from the updatingObject
.
let mainObject = {
age: 24,
isMarried: false,
pets: {
dog: {
name: 'Juniper',
age: 3,
},
cat: {
name: 'Spasia',
age: 7,
},
},
hobbies: {
mountainRelated: ['hiking', 'snowboarding'],
},
};
let updatingObject = {
pets: {
cat: {
age: 8,
},
},
hobbies: {
mountainRelated: ['biking'],
none: 'not updated',
},
};
function updateGivenObject(mainObject, updatingObject) {
const mainObjPaths = [];
const updateObjPaths = [];
buildPath(mainObject, mainObjPaths);
buildPath(updatingObject, updateObjPaths);
console.log('mainObjPaths:', mainObjPaths);
console.log('updateObjPaths :', updateObjPaths );
updateObjPaths.forEach(path => {
const newValue = getPropByPath(updatingObject, path);
setPropByPath(mainObject, path, newValue);
});
}
function buildPath(obj, accumulatedPaths, currentPaths = []) {
Object.keys(obj).map(key => {
if (typeof obj[key] !== 'object') {
accumulatedPaths.push([...currentPaths, key].join('.'));
} else {
buildPath(obj[key], accumulatedPaths, [...currentPaths, key]);
}
});
}
function getPropByPath(obj, path) {
const pathArr = path.split('.');
let value = obj;
pathArr.forEach(key => {
value = value[key];
});
return value;
}
function setPropByPath(obj, path, newValue) {
const pathArr = path.split('.');
let value = obj;
pathArr.forEach((key, idx) => {
if (value[key] === undefined) {
return;
}
if (idx === pathArr.length - 1) {
value[key] = newValue;
} else {
value = value[key];
}
});
return value;
}
updateGivenObject(mainObject, updatingObject);
console.log(mainObject);
console.log(mainObject.pets.cat.age);
console.log(mainObject.hobbies.mountainRelated[0]);
console.log(mainObject.hobbies.none);
However, the above algorithm depends heavily on shared mutable state and should be careful about it.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Scott Sauyet |
Solution 3 | Ray Chan |