Access dynamic nested key in JS object - javascript

I have an array like ['animals', 'cats', 'cute', 'fast', 'small', ...], and want to access nested keys of the object like
let object = {
one: {
two: {
three: {
// and so on
}
}
}
}
Usually I would write object['animals']['cats']['cute']['fast']['small']..
The problem is that keys and the number of levels are dynamic (so I can get objects with 2 nested levels or 50), so I have no idea how it can be done
Thanks in advance for any help

Iterate over the array of keys with .reduce, where the accumulator is the current nested object:
let object = {
one: {
two: {
three: {
prop: 'val'
}
}
}
};
const props = ['one', 'two', 'three', 'prop'];
const nestedVal = props.reduce((a, prop) => a[prop], object);
console.log(nestedVal);
To assign a value at the same point, first pop off the last key, use the same reduce trick to get to the last object, and assign to the property at the last key with bracket notation:
let object = {
one: {
two: {
three: {
prop: 'val'
}
}
}
};
const props = ['one', 'two', 'three', 'prop'];
const lastKey = props.pop();
const nestedObj = props.reduce((a, prop) => a[prop], object);
nestedObj[lastKey] = 'newVal';
console.log(object);

Related

Can destructuring an array be used to map properties of each elements?

Suppose there is an array like this:
const a = [ {p:1}, {p:2}, {p:3} ];
Is it possible to destructure this array in order to obtain p = [1, 2, 3] ?
Because this does not work :
const [ ...{ p } ] = a; // no error, same as const p = a.p;
// p = undefined;
Edit
In response to all the answers saying that I need to use Array.prototype.map, I am aware of this. I was simply wondering if there was a way to map during the destructuring process, and the answer is : no, I need to destructure the array itself, then use map as a separate step.
For example:
const data = {
id: 123,
name: 'John',
attributes: [{ id:300, label:'attrA' }, { id:301, label:'attrB' }]
};
function format(data) {
const { id, name, attributes } = data;
const attr = attributes.map(({ label }) => label);
return { id, name, attr };
}
console.log( format(data) };
// { id:123, name:'John', attr:['attrA', 'attrB'] }
I was simply wondering if there was a way, directly during destructuring, without using map (and, respectfully, without the bloated lodash library), to retrive all label properties into an array of strings.
Honestly I think that what you are looking for doesn't exist, normally you would map the array to create a new array using values from properties. In this specific case it would be like this
const p = a.map(element => element.p)
Of course, there are some packages that have many utilities to help, like Lodash's map function with the 'property' iteratee
you can destructure the first item like this :
const [{ p }] = a;
but for getting all values you need to use .map
and the simplest way might be this :
const val = a.map(({p}) => p)
Here's a generalized solution that groups all properties into arrays, letting you destructure any property:
const group = (array) => array.reduce((acc,obj) => {
for(let [key,val] of Object.entries(obj)){
acc[key] ||= [];
acc[key].push(val)
}
return acc
}, {})
const ar = [ {p:1}, {p:2}, {p:3} ];
const {p} = group(ar)
console.log(p)
const ar2 = [{a:2,b:1},{a:5,b:4}, {c:1}]
const {a,b,c} = group(ar2)
console.log(a,b,c)

How can we create OBJ property with the same name from array values?

Hello I'm trying to create an object that includes under the same property name a bunch of array values,
This what I'm trying
const quiz = [
{question: 'Who is the main character of DBZ',
options: ['Vegetta','Gohan','Goku']}
]
const newObj = {
options: []
}
quiz.forEach((item)=>{
item.options.forEach((item, index)=>{
newObj.options[`name${index}`] = item
})
})
expected value =
newObj = {
options: [{name: 'Vegetta'},{name:'Gohan'},{name:'Goku'}]
}
actual value received =
newObj = {
{ options: [ name0: 'Vegetta', name1: 'Gohan', name2: 'Goku' ] }}
Thanks in advance!
As you've noticed, newObj.options[`name${index}`] = item creates a new key on your options array, and sets that to item. You instead want to push an object of the form {name: item} into your array. There are a few ways you could go about this, one way is to use .push() like so:
quiz.forEach((item)=>{
item.options.forEach((item)=>{
newObj.options.push({name: item});
})
});
while not as common, you can also use set the current index of options, which is slightly different to the above example, as it will maintain the same index, which can be important if quiz is a sparse array that you want to keep the same indexing of on options:
quiz.forEach((item)=>{
item.options.forEach((item, index)=>{
newObj.options[index] = {name: item};
})
});
Example of the difference:
const arr = [1, 2,,,5]; // sparse array
const pushRes = [];
const indxRes = [];
arr.forEach(n => pushRes.push(n));
arr.forEach((n, i) => indxRes[i] = n);
console.log("Push result", pushRes);
console.log("Index result", indxRes);
For a different approach, you also have the option of using something like .flatMap() and .map() to create your options array, which you can use to create newObj:
const quiz = [
{question: 'Who is the main character of DBZ',
options: ['Vegetta','Gohan','Goku']}
];
const options = quiz.flatMap(({options}) => options.map(name => ({name})));
const newObj = {options};
console.log(newObj);

Get equals values from two nested objects

Suppose I have two objects like these ones:
const obj1 = {
ch1: {
as: ['a', 'b'],
ns: ['1']
}
ch2: {
ss: ['#', '*', '+'],
ts: ['2', '3']
}
}
const obj2 = {
ch1: {
as: ['a', 'g'],
}
ch2: {
ss: ['#', '-', '+'],
ts: ['1', '5']
}
}
const result = findSimilarities(obj1, obj2)
should returns:
[
'a' // because obj1.ch1.as and obj2.ch1.as contain 'a'
'#' // because obj1.ch2.ss and obj2.ch2.ss contain '#'
'+' // because obj1.ch2.ss and obj2.ch2.ss contain '+'
]
I can define Level 1 as ch1 or ch2, Level 2 as as, ns, ss or ts and Level 3 the strings inside the arrays.
So what I'm interested in are che value at level 3 that the two objects have in common.
Honestly I didn't know how to start..
What I would do is start with a function, function getMatches(obj1, obj2) { ....
Iterate over the first-level keys of obj1 with Object.keys(obj1).foreach( key2 => .... (2 to represent what you call level 2)
Then, iterate over THOSE keys, Object.keys(obj1[key2]).foreach(key3 > ...
So now you have an array that you can access via obj1[key2][key3], and you can access the array to compare against via obj2[key2][key3]. So now you just have to iterate every value in obj1[key2][key3] and check if it's in obj2[key2][key3]. If a key is in both, smash it in an array, and then return the array at the end of the function
[edit] you may have to use some sort of check, or the optional chaining operator ?., to access the keys on obj2, in case there's a key on obj1 that isn't on obj2
Here is what I would recommend:
Create a function lets call it function compare(obj1, obj2), then create two Object.values variables followed by an empty array var equivalent = [];
After that have a forEach function that searches both objects, and the final product should look something like:
function compare(obj1, obj2) {
var values1 = Object.values(obj1);
var values2 = Object.values(obj2);
var equivalent = [];
var keys = Object.keys(obj1);
keys.forEach(k => {
if (obj1.hasOwnProperty(k) && obj2.hasOwnProperty(k)) {
if (obj1[k] === obj2[k]) {
equivalent.push(obj1[k]);
}
}
});
console.log(equivalent);
}
compare(obj1, obj2);
I also want to leave my solution here. It will work properly on the data structures like you described above. On whatever object depth and if arrays will contains only primitive values.
function findSimilarities(obj1, obj2) {
if (obj1 instanceof Array && obj2 instanceof Array) {
return findSimilarArrayValues(obj1, obj2)
}
if (!obj1 instanceof Object || !obj1 instanceof Object) {
return [];
}
const similarKeys = findSimilarObjectKeys(obj1, obj2);
return similarKeys.map(key => findSimilarities(obj1[key], obj2[key])).flat()
}
function findSimilarObjectKeys(obj1, obj2) {
return Object.keys(obj1).filter(key => obj2.hasOwnProperty(key));
}
function findSimilarArrayValues(arr1, arr2) {
return arr1.filter(item => arr2.includes(item));
}
Try to start with Object.keys, Object.entries and Object.values.
Good luck!
Use Object.keys to find all keys in first object, Object.protoype.hasOwnProperty() to check if these keys exist in object 2
Repeat that for levels 1 and 2
Extract the values common to level 3 arrays using Array.prototype.filter() on the array from first object to keep only items found in its match from second object:
function findSimilarities(x, y) {
const res = [];
for (const k1 of Object.keys(x)) { //Level 1
if (y.hasOwnProperty(k1)) {
const xo1 = x[k1];
const yo1 = y[k1];
for (const k2 of Object.keys(xo1)) { //Level 2
if (yo1.hasOwnProperty(k2)) {
const xo2 = xo1[k2];
const yo2 = yo1[k2];
if (Array.isArray(xo2) && Array.isArray(yo2)) { //Level 3 are arrays
const dupXo2 = xo2.filter(el => yo2.includes(el));
res.push(...dupXo2);
}
}
}
}
}
return res;
}
const obj1 = {
ch1: {
as: ['a', 'b'],
ns: ['1']
},
ch2: {
ss: ['#', '*', '+'],
ts: ['2', '3']
}
};
const obj2 = {
ch1: {
as: ['a', 'g'],
},
ch2: {
ss: ['#', '-', '+'],
ts: ['1', '5']
}
};
const result = findSimilarities(obj1, obj2);
console.log(result);

If two different objects have same key name, merge their value

let obj1 = { names: ["Zack","Cody"]};
let obj2 = { names: ["John","Jake"] };
Results in: obj1 = { names: ["Zack","Cody","John","Jake"]}
What I have tried:
if (Object.keys(obj1) == Object.keys(obj2)) {
Object.values(obj1) = [...Object.values(obj1), ...Object.values(obj2)];
}
Iterate through the keys in obj1, and for every one that exists in obj2, merge its values into obj1:
let obj1 = { names: ["Zack","Cody"]};
let obj2 = { names: ["John","Jake"] };
Object.keys(obj1).forEach(function(k) {
if ( obj2[k] ) { // obj2 contains this key
obj1[k].push( ...obj2[k] ) // Add the values from obj2's key to obj1's key
}
});
You can just iterate over each objects properties and add them to the new object if that property does not exist. To deal with merges where the same key exists in other objects we can define a merge strategy function that will resolve the conflict.
let obj1 = { names: ["Zack","Cody"]};
let obj2 = { names: ["John","Jake"] };
function mergeObjs(objects, mergeStrategy) {
mergeStrategy = mergeStrategy || ((oldV, newV) => newV);
const result = {};
for (let ob of objects) {
for (let [k, v] of Object.entries(ob)) {
const oldV = result[k];
result[k] = (oldV == undefined) ? v: mergeStrategy(oldV, v);
}
}
return result;
}
console.log(mergeObjs([obj1, obj2], (oldV, newV) => oldV.concat(newV)));
the function only allows one strategy per merge, which might be a bit limiting for more complex cases but for simple ones like this its ok.
You could concat the value by wrapping into array element and flatten (or concat as #ggorlen suggested in the comment) it
let obj1 = { names: ["Zack", "Cody"] }
let obj2 = { names: ["John", "Jake"] }
const mergeObj = (obj1Raw = {}, obj2Raw = {}) => {
// shallow clone for demo only
let obj1 = { ...obj1Raw }
let obj2 = { ...obj2Raw }
for (const prop in obj2) {
if (obj1.hasOwnProperty(prop)) {
obj1[prop] = [obj1[prop], obj2[prop]].flat()
} else {
obj1[prop] = obj2[prop]
}
}
return obj1
}
console.log(mergeObj(obj1, obj2))
You can try lodash.merge, powerful function to merge multiple objects
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 }] }
Here's one-liner using Object.entries, Object.fromEntries and Array#map, assuming the properties share the same key name are always arrays:
let obj1 = { names: ["Zack","Cody"] };
let obj2 = { names: ["John","Jake"] };
const result = {
...obj1,
...Object.fromEntries(Object.entries(obj2).map(([k, v]) => [
k, obj1[k] ? [...obj1[k], ...v] : v
]))
};
console.log(result);
In your code, you were comparing two arrays - Object.keys(obj1) and Object.keys(obj2). This condition would never be true because no two arrays are equal because they have reference to different memory location.
To deal with this, you can compare their contents. This is what I am doing in the below snippet -
let obj1 = { names: ["Zack","Cody"]};
let obj2 = { names: ["John","Jake"]};
if (Object.keys(obj1)[0] == Object.keys(obj2)[0]) {
Object.values(obj1)[0].push(...Object.values(obj2)[0]);
console.log(obj1.names);
}
I am only comparing the first keys in both objects in the above snippet. If you like to do this for every key in both objects, then you can use a loop with indexes replacing the [0].
Note - If you are going to use the above approach, then the order of keys in both objects matters.

find and modify deeply nested object in javascript array

I have an array of objects that can be of any length and any depth. I need to be able to find an object by its id and then modify that object within the array. Is there an efficient way to do this with either lodash or pure js?
I thought I could create an array of indexes that led to the object but constructing the expression to access the object with these indexes seems overly complex / unnecessary
edit1; thanks for all yours replies I will try and be more specific. i am currently finding the location of the object I am trying to modify like so. parents is an array of ids for each parent the target object has. ancestors might be a better name for this array. costCenters is the array of objects that contains the object I want to modify. this function recurses and returns an array of indexes that lead to the object I want to modify
var findAncestorsIdxs = function(parents, costCenters, startingIdx, parentsIdxs) {
var idx = startingIdx ? startingIdx : 0;
var pidx = parentsIdxs ? parentsIdxs : [];
_.each(costCenters, function(cc, ccIdx) {
if(cc.id === parents[idx]) {
console.log(pidx);
idx = idx + 1;
pidx.push(ccIdx);
console.log(pidx);
pidx = findAncestorsIdx(parents, costCenters[ccIdx].children, idx, pidx);
}
});
return pidx;
};
Now with this array of indexes how do I target and modify the exact object I want? I have tried this where ancestors is the array of indexes, costCenters is the array with the object to be modified and parent is the new value to be assigned to the target object
var setParentThroughAncestors = function(ancestors, costCenters, parent) {
var ccs = costCenters;
var depth = ancestors.length;
var ancestor = costCenters[ancestors[0]];
for(i = 1; i < depth; i++) {
ancestor = ancestor.children[ancestors[i]];
}
ancestor = parent;
console.log(ccs);
return ccs;
};
this is obviously just returning the unmodified costCenters array so the only other way I can see to target that object is to construct the expression like myObjects[idx1].children[2].grandchildren[3].ggranchildren[4].something = newValue. is that the only way? if so what is the best way to do that?
You can use JSON.stringify for this. It provides a callback for each visited key/value pair (at any depth), with the ability to skip or replace.
The function below returns a function which searches for objects with the specified ID and invokes the specified transform callback on them:
function scan(id, transform) {
return function(obj) {
return JSON.parse(JSON.stringify(obj, function(key, value) {
if (typeof value === 'object' && value !== null && value.id === id) {
return transform(value);
} else {
return value;
}
}));
}
If as the problem is stated, you have an array of objects, and a parallel array of ids in each object whose containing objects are to be modified, and an array of transformation functions, then it's just a matter of wrapping the above as
for (i = 0; i < objects.length; i++) {
scan(ids[i], transforms[i])(objects[i]);
}
Due to restrictions on JSON.stringify, this approach will fail if there are circular references in the object, and omit functions, regexps, and symbol-keyed properties if you care.
See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_native_JSON#The_replacer_parameter for more info.
As Felix Kling said, you can iterate recursively over all objects.
// Overly-complex array
var myArray = {
keyOne: {},
keyTwo: {
myId: {a: '3'}
}
};
var searchId = 'myId', // Your search key
foundValue, // Populated with the searched object
found = false; // Internal flag for iterate()
// Recursive function searching through array
function iterate(haystack) {
if (typeof haystack !== 'object' || haystack === null) return; // type-safety
if (typeof haystack[searchId] !== 'undefined') {
found = true;
foundValue = haystack[searchId];
return;
} else {
for (var i in haystack) {
// avoid circular reference infinite loop & skip inherited properties
if (haystack===haystack[i] || !haystack.hasOwnProperty(i)) continue;
iterate(haystack[i]);
if (found === true) return;
}
}
}
// USAGE / RESULT
iterate(myArray);
console.log(foundValue); // {a: '3'}
foundValue.b = 4; // Updating foundValue also updates myArray
console.log(myArray.keyTwo.myId); // {a: '3', b: 4}
All JS object assignations are passed as reference in JS. See this for a complete tutorial on objects :)
Edit: Thanks #torazaburo for suggestions for a better code.
If each object has property with the same name that stores other nested objects, you can use: https://github.com/dominik791/obj-traverse
findAndModifyFirst() method should solve your problem. The first parameter is a root object, not array, so you should create it at first:
var rootObj = {
name: 'rootObject',
children: [
{
'name': 'child1',
children: [ ... ]
},
{
'name': 'child2',
children: [ ... ]
}
]
};
Then use findAndModifyFirst() method:
findAndModifyFirst(rootObj, 'children', { id: 1 }, replacementObject)
replacementObject is whatever object that should replace the object that has id equal to 1.
You can try it using demo app:
https://dominik791.github.io/obj-traverse-demo/
Here's an example that extensively uses lodash. It enables you to transform a deeply nested value based on its key or its value.
const _ = require("lodash")
const flattenKeys = (obj, path = []) => (!_.isObject(obj) ? { [path.join('.')]: obj } : _.reduce(obj, (cum, next, key) => _.merge(cum, flattenKeys(next, [...path, key])), {}));
const registrations = [{
key: "123",
responses:
{
category: 'first',
},
}]
function jsonTransform (json, conditionFn, modifyFn) {
// transform { responses: { category: 'first' } } to { 'responses.category': 'first' }
const flattenedKeys = Object.keys(flattenKeys(json));
// Easily iterate over the flat json
for(let i = 0; i < flattenedKeys.length; i++) {
const key = flattenedKeys[i];
const value = _.get(json, key)
// Did the condition match the one we passed?
if(conditionFn(key, value)) {
// Replace the value to the new one
_.set(json, key, modifyFn(key, value))
}
}
return json
}
// Let's transform all 'first' values to 'FIRST'
const modifiedCategory = jsonTransform(registrations, (key, value) => value === "first", (key, value) => value = value.toUpperCase())
console.log('modifiedCategory --', modifiedCategory)
// Outputs: modifiedCategory -- [ { key: '123', responses: { category: 'FIRST' } } ]
I needed to modify deeply nested objects too, and found no acceptable tool for that purpose. Then I've made this and pushed it to npm.
https://www.npmjs.com/package/find-and
This small [TypeScript-friendly] lib can help with modifying nested objects in a lodash manner. E.g.,
var findAnd = require("find-and");
const data = {
name: 'One',
description: 'Description',
children: [
{
id: 1,
name: 'Two',
},
{
id: 2,
name: 'Three',
},
],
};
findAnd.changeProps(data, { id: 2 }, { name: 'Foo' });
outputs
{
name: 'One',
description: 'Description',
children: [
{
id: 1,
name: 'Two',
},
{
id: 2,
name: 'Foo',
},
],
}
https://runkit.com/embed/bn2hpyfex60e
Hope this could help someone else.
I wrote this code recently to do exactly this, as my backend is rails and wants keys like:
first_name
and my front end is react, so keys are like:
firstName
And these keys are almost always deeply nested:
user: {
firstName: "Bob",
lastName: "Smith",
email: "bob#email.com"
}
Becomes:
user: {
first_name: "Bob",
last_name: "Smith",
email: "bob#email.com"
}
Here is the code
function snakeCase(camelCase) {
return camelCase.replace(/([A-Z])/g, "_$1").toLowerCase()
}
export function snakeCasedObj(obj) {
return Object.keys(obj).reduce(
(acc, key) => ({
...acc,
[snakeCase(key)]: typeof obj[key] === "object" ? snakeCasedObj(obj[key]) : obj[key],
}), {},
);
}
Feel free to change the transform to whatever makes sense for you!

Categories