Related
I have an object like below in Javascript,
const obj = {
"group1": {
"sub_group1_1": {
"inner_sub_group1_1_1": {
"name": "abc"
},
"inner_sub_group1_1_2": {
"name": "def"
}
},
"sub_group1_2": {
"inner_sub_group1_2_1": {
"name": "ghi"
},
"inner_sub_group1_2_2": {
"name": "jkl"
}
}
},
"group2": {
"sub_group2_1": {
"inner_sub_group2_1_1": {
"name": "mno"
},
"inner_sub_group2_1_2": {
"name": "pqr"
}
},
"sub_group2_2": {
"inner_sub_group2_2_1": {
"name": "stu"
},
"inner_sub_group2_2_2": {
"name": "wxy"
}
}
}
}
I want to write a function which could search for a key in the above object and return me the corresponding value of that key, for e.g.,
Input
filterObject(obj, 'inner_sub_group2_2_2')
Output
{
"name": "wxy
}
Input
filterObject(obj, 'inner_sub_group1_1_1')
Output
{
"name": "abc
}
Approach
I have written a recursive function for the same, but it doesn't seem to work for the second scenario,
const filterObject = (obj, searchedKey) => {
let result = {};
for (const key in obj) {
const currentObj = obj[key];
if (key === searchedKey) {
result = currentObj;
break;
} else if (typeof currentObj === 'object') {
result = filterObject(currentObj, searchedKey);
}
}
return result;
};
Any sort of help would be highly appreciated. Thanks!
You need to be able to check whether the recursive call found something, which you aren't doing - right now, you're just reassigning result, and then ignoring it and going on to the next iteration of the loop.
An approach that would work for this particular situation would be to check to see if the resulting object has any keys.
const obj={group1:{sub_group1_1:{inner_sub_group1_1_1:{name:"abc"},inner_sub_group1_1_2:{name:"def"}},sub_group1_2:{inner_sub_group1_2_1:{name:"ghi"},inner_sub_group1_2_2:{name:"jkl"}}},group2:{sub_group2_1:{inner_sub_group2_1_1:{name:"mno"},inner_sub_group2_1_2:{name:"pqr"}},sub_group2_2:{inner_sub_group2_2_1:{name:"stu"},inner_sub_group2_2_2:{name:"wxy"}}}};
const filterObject = (obj, searchedKey) => {
if (searchedKey in obj) {
return obj[searchedKey];
}
for (const key in obj) {
const currentObj = obj[key];
if (typeof currentObj === 'object') {
const result = filterObject(currentObj, searchedKey);
if (Object.keys(result).length) return result;
}
}
return {};
};
console.log(filterObject(obj, 'inner_sub_group2_2_2'));
console.log(filterObject(obj, 'inner_sub_group1_1_1'));
But that won't work if the value found isn't an object, or if the found object is empty. (A different object reference unconnected with the original structure would be returned.)
For the more general situation, the recursive call should return two things: whether the nested value was found or not, and the found value (if any). One approach to this is to return an object if something was found, and nothing otherwise.
const obj={group1:{sub_group1_1:{inner_sub_group1_1_1:{name:"abc"},inner_sub_group1_1_2:{name:"def"}},sub_group1_2:{inner_sub_group1_2_1:{name:"ghi"},inner_sub_group1_2_2:{name:"jkl"}}},group2:{sub_group2_1:{inner_sub_group2_1_1:{name:"mno"},inner_sub_group2_1_2:{name:"pqr"}},sub_group2_2:{inner_sub_group2_2_1:{name:"stu"},inner_sub_group2_2_2:{name:"wxy"}}}};
const filterObject = (obj, searchedKey) => {
if (searchedKey in obj) {
return { result: obj[searchedKey] };
}
for (const key in obj) {
const currentObj = obj[key];
if (typeof currentObj === 'object') {
const result = filterObject(currentObj, searchedKey);
if (result) return result;
}
}
};
console.log(filterObject(obj, 'inner_sub_group2_2_2').result);
console.log(filterObject(obj, 'inner_sub_group1_1_1').result);
The issue in your code is that if the recursive call is successful, the loop still continues, with another recursive call being made that may not be successful, and so the good result is lost.
You need an if after that recursive call that detects success and breaks out if it is one.
Now, to make the distinction between failure and success you'll make things much easier if you return null when there is no success.
Two lines need to be adjusted:
const filterObject = (obj, searchedKey) => {
let result = null; // To indicate nothing found yet
for (const key in obj) {
const currentObj = obj[key];
if (key === searchedKey) {
result = currentObj;
break;
} else if (typeof currentObj === 'object') {
result = filterObject(currentObj, searchedKey);
if (result) break; // <--- break out when success!
}
}
return result;
};
A little improvement might be needed on your is-object test, because typeof null === 'object' is a true expression.
You can use this instead:
if (Object(currentObj) === currentObj)
Here is a variation of #trincot's and #certainperformance's answers. Nothing fundamentally new, just a slightly different style:
const obj={group1:{sub_group1_1:{inner_sub_group1_1_1:{name:"abc"},inner_sub_group1_1_2:{name:"def"}},sub_group1_2:{inner_sub_group1_2_1:{name:"ghi"},inner_sub_group1_2_2:{name:"jkl",spoiler:null}}},group2:{sub_group2_1:{inner_sub_group2_1_1:{name:"mno"},inner_sub_group2_1_2:{name:"pqr",xtr:false}},sub_group2_2:{inner_sub_group2_2_1:{name:"stu"},inner_sub_group2_2_2:{name:"wxy"}}}};
function deepGet(obj,k,r){ // call with two arguments only
if(obj&&typeof obj=="object") {
if(k in obj) return obj[k];
for(l in obj) if((r=deepGet(obj[l],k))!=null) return r;
}
return null;
}
console.log(['inner_sub_group2_2_2','inner_sub_group1_1_1','xtr'].map(k=>deepGet(obj,k)));
i have an nested object as such:
options = {
religous: {
kosher: {
value: 'Kosher',
chosen: false
},
halal: {
value: 'Halal',
active: false
},
},
vegan: {
value: 'Vegan',
active: false
}
}
It contains nested objects of varying sizes. I would like to get an Array containing the values of any value propery. So for the above object the desired output would be:
['Kosher', 'Halal', 'Vegan']
Order doesn't really matter.
I tried to do so recursively as such:
getListOfLabels = obj => {
const lst = []
for (let key in obj) {
if (obj[key].value) lst.push(obj[key].value)
else return getListOfLabels(obj[key])
}
return lst
}
but I keep getting a RangeError: Maximum call stack size exceeded error.
Any suggestions?
The for...in loop assigns the key. To get the value use obj[key]. If the key is value add to lst, if it's an object, call getListOfLabels on it, and spread the results into lst.push():
const options = {"religous":{"kosher":{"value":"Kosher","chosen":false},"halal":{"value":"Halal","active":false}},"vegan":{"value":"Vegan","active":false}}
const getListOfLabels = obj => {
const lst = []
for (let key in obj) {
const val = obj[key] // get the value
if (key === 'value') lst.push(val) // if the key name is "value" push to lst
else if(typeof val === 'object') lst.push(...getListOfLabels(val)) // if type of value is object, iterate it with getListOfLabels and push the results into lst
}
return lst
}
const result = getListOfLabels(options)
console.log(result)
You could take a recursive approach and check if the object contains a value key.
function getValues(object, key) {
if (key in object) return [object[key]];
return Object.values(object).reduce((r, v) => {
if (v && typeof v === 'object') r.push(...getValues(v, key));
return r;
}, []);
}
var options = { religous: { kosher: { value: 'Kosher', chosen: false }, halal: { value: 'Halal', active: false } }, vegan: { value: 'Vegan', active: false } };
console.log(getValues(options, 'value'));
Here's a succinct approach using reduce :-D
const getValues = options => Object.values(options)
.reduce((acc, optionObj) => (
optionObj.value ? [ ...acc, optionObj.value ] : [
...acc,
...Object.values(optionObj).reduce((arr, { value }) => ([ ...arr, value ]), [])
]), [])
function updateObjectWithKeyAndValue (object, key, value) {
object = { [key]: value };
let new_object = {[key] : value};
return Object.assign(object, new_object);
}
Error:
Objects updateObjectWithKeyAndValue(object, key, value) returns an object with the original key value pairs and the new key value pair:
Error: Expected { prop2: 2 } to match { prop: 1, prop2: 2 }
If you want your function to update provided object with specific key and value, you just need this:
function updateObjectWithKeyAndValue (object, key, value) {
if (typeof object === 'object' && object !== null) {
object[key] = value;
}
}
I figured this must be a dup, but I can't find it on SO. Given an object like this:
let obj = { keyA: { keyB: 'hi', keyC: { keyD: null } }, keyE: 'hi' }
Is there a way I can find key paths to a given value, like this:
keyPaths(obj, 'hi') // -> [ 'keyA.keyB', 'keyE' ]
keyPaths(obj) // -> [ 'keyA.keyB.keyD' ]
I tried to adapt some of the answers that find deep values knowing the key, and I was almost able to adapt this one that finds deep nulls, but I can't figure out how to get the path back, instead of just the deepest key.
I would go with a depth first search like this :
let obj = { keyA: { keyB: 'hi', keyC: { keyD: null } }, keyE: 'hi' }
function keyPaths(parent, value = null, chain) {
let allResults = [];
for (const prop in parent) {
if (parent.hasOwnProperty(prop)) {
const element = parent[prop];
const newChain = chain ? chain + '.' + prop : prop;
if (element === value) {
allResults.push(newChain);
}
else if (Object.keys(prop).length > 1) {
allResults = [...allResults, ...keyPaths(element, value, newChain)];
}
}
}
return allResults;
}
console.log(keyPaths(obj, 'hi')) // -> [ 'keyA.keyB', 'keyE' ]
console.log(keyPaths(obj)) // -> [ 'keyA.keyB.keyC' ]
Basically, I check all the properties of the given element for a matching value. If a property doesn't match the value, but has child properties, I recursively call the function, and merge the results from the call iteration and the recursive call.
You do this pretty cleanly by using reduce inside a recursive function. The function will return an array, which you can than map() to whatever string values you want.
let obj = { keyA: { keyB: 'hi', keyC: { keyD: null } }, keyE: 'hi' }
function keyPaths(obj, val, path = [] ){
if (!obj) return
return Object.entries(obj).reduce((res, [k, v]) => {
let p = [...path, k]
if (v == val) res.push(p)
else if (v && typeof v == 'object') res.push(...keyPaths(v, val, p))
return res
}, [])
}
console.log(keyPaths(obj, 'hi').map(a => a.join('.')))
console.log(keyPaths(obj).map(a => a.join('|')))
If it's ok to use Lodash+Deepdash, then:
let paths = _(obj).filterDeep((v)=>v=='hi').paths().value();
Codepen is here
I have this code in js bin:
var validator = {
set (target, key, value) {
console.log(target);
console.log(key);
console.log(value);
if(isObject(target[key])){
}
return true
}
}
var person = {
firstName: "alfred",
lastName: "john",
inner: {
salary: 8250,
Proffesion: ".NET Developer"
}
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'
if i do proxy.inner.salary = 555; it does not work.
However if i do proxy.firstName = "Anne", then it works great.
I do not understand why it does not work Recursively.
http://jsbin.com/dinerotiwe/edit?html,js,console
You can add a get trap and return a new proxy with validator as a handler:
var validator = {
get(target, key) {
if (typeof target[key] === 'object' && target[key] !== null) {
return new Proxy(target[key], validator)
} else {
return target[key];
}
},
set (target, key, value) {
console.log(target);
console.log(key);
console.log(value);
return true
}
}
var person = {
firstName: "alfred",
lastName: "john",
inner: {
salary: 8250,
Proffesion: ".NET Developer"
}
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'
A slight modification on the example by Michał Perłakowski with the benefit of this approach being that the nested proxy is only created once rather than every time a value is accessed.
If the property of the proxy being accessed is an object or array, the value of the property is replaced with another proxy. The isProxy property in the getter is used to detect whether the currently accessed object is a proxy or not. You may want to change the name of isProxy to avoid naming collisions with properties of stored objects.
Note: the nested proxy is defined in the getter rather than the setter so it is only created if the data is actually used somewhere. This may or may not suit your use-case.
const handler = {
get(target, key) {
if (key == 'isProxy')
return true;
const prop = target[key];
// return if property not found
if (typeof prop == 'undefined')
return;
// set value as proxy if object
if (!prop.isProxy && typeof prop === 'object')
target[key] = new Proxy(prop, handler);
return target[key];
},
set(target, key, value) {
console.log('Setting', target, `.${key} to equal`, value);
// todo : call callback
target[key] = value;
return true;
}
};
const test = {
string: "data",
number: 231321,
object: {
string: "data",
number: 32434
},
array: [
1, 2, 3, 4, 5
],
};
const proxy = new Proxy(test, handler);
console.log(proxy);
console.log(proxy.string); // "data"
proxy.string = "Hello";
console.log(proxy.string); // "Hello"
console.log(proxy.object); // { "string": "data", "number": 32434 }
proxy.object.string = "World";
console.log(proxy.object.string); // "World"
I published a library on GitHub that does this as well. It will also report to a callback function what modifications have taken place along with their full path.
Michal's answer is good, but it creates a new Proxy every time a nested object is accessed. Depending on your usage, that could lead to a very large memory overhead.
I have also created a library type function for observing updates on deeply nested proxy objects (I created it for use as a one-way bound data model). Compared to Elliot's library it's slightly easier to understand at < 100 lines. Moreover, I think Elliot's worry about new Proxy objects being made is a premature optimisation, so I kept that feature to make it simpler to reason about the function of the code.
observable-model.js
let ObservableModel = (function () {
/*
* observableValidation: This is a validation handler for the observable model construct.
* It allows objects to be created with deeply nested object hierarchies, each of which
* is a proxy implementing the observable validator. It uses markers to track the path an update to the object takes
* <path> is an array of values representing the breadcrumb trail of object properties up until the final get/set action
* <rootTarget> the earliest property in this <path> which contained an observers array *
*/
let observableValidation = {
get(target, prop) {
this.updateMarkers(target, prop);
if (target[prop] && typeof target[prop] === 'object') {
target[prop] = new Proxy(target[prop], observableValidation);
return new Proxy(target[prop], observableValidation);
} else {
return target[prop];
}
},
set(target, prop, value) {
this.updateMarkers(target, prop);
// user is attempting to update an entire observable field
// so maintain the observers array
target[prop] = this.path.length === 1 && prop !== 'length'
? Object.assign(value, { observers: target[prop].observers })
: value;
// don't send events on observer changes / magic length changes
if(!this.path.includes('observers') && prop !== 'length') {
this.rootTarget.observers.forEach(o => o.onEvent(this.path, value));
}
// reset the markers
this.rootTarget = undefined;
this.path.length = 0;
return true;
},
updateMarkers(target, prop) {
this.path.push(prop);
this.rootTarget = this.path.length === 1 && prop !== 'length'
? target[prop]
: target;
},
path: [],
set rootTarget(target) {
if(typeof target === 'undefined') {
this._rootTarget = undefined;
}
else if(!this._rootTarget && target.hasOwnProperty('observers')) {
this._rootTarget = Object.assign({}, target);
}
},
get rootTarget() {
return this._rootTarget;
}
};
/*
* create: Creates an object with keys governed by the fields array
* The value at each key is an object with an observers array
*/
function create(fields) {
let observableModel = {};
fields.forEach(f => observableModel[f] = { observers: [] });
return new Proxy(observableModel, observableValidation);
}
return {create: create};
})();
It's then trivial to create an observable model and register observers:
app.js
// give the create function a list of fields to convert into observables
let model = ObservableModel.create([
'profile',
'availableGames'
]);
// define the observer handler. it must have an onEvent function
// to handle events sent by the model
let profileObserver = {
onEvent(field, newValue) {
console.log(
'handling profile event: \n\tfield: %s\n\tnewValue: %s',
JSON.stringify(field),
JSON.stringify(newValue));
}
};
// register the observer on the profile field of the model
model.profile.observers.push(profileObserver);
// make a change to profile - the observer prints:
// handling profile event:
// field: ["profile"]
// newValue: {"name":{"first":"foo","last":"bar"},"observers":[{}
// ]}
model.profile = {name: {first: 'foo', last: 'bar'}};
// make a change to available games - no listeners are registered, so all
// it does is change the model, nothing else
model.availableGames['1234'] = {players: []};
Hope this is useful!
I wrote a function based on Michał Perłakowski code. I added access to the path of property in the set/get functions. Also, I added types.
const createHander = <T>(path: string[] = []) => ({
get: (target: T, key: keyof T): any => {
if (key == 'isProxy') return true;
if (typeof target[key] === 'object' && target[key] != null)
return new Proxy(
target[key],
createHander<any>([...path, key as string])
);
return target[key];
},
set: (target: T, key: keyof T, value: any) => {
console.log(`Setting ${[...path, key]} to: `, value);
target[key] = value;
return true;
}
});
const proxy = new Proxy(obj ,createHander<ObjectType>());