Find object by properties from array - javascript

With an array, a value, and and an object with nested objects:
Object
mesh
Array
['options', 'range', 'x']
Value
12.5
Is it possible to translate this to update a property, e.g.
mesh.options.range.x = 12.5
Attempted:
index = (obj, i) ->
obj[i]
arr.reduce(index, obj) = 12.5
Update
Thank you all for the elegant solutions.

Using .reduce() is actually pretty nice for this:
// current object----| |----current key
// v v
arr.reduce(function(obj, key) {
return obj == null ? obj : obj[key];
}, window.mesh);
// ^
// |-- initial object
Your attempt to use .reduce() needed to pass a function that manages the "accumulation".
Here, as long as the previous obj wasn't null or undefined, it'll return the key of the current obj, which becomes the next obj.
Then since you need to assign a value, you'd actually want to get the value of the second to last key.
var o = arr.slice(0,-1).reduce(function(obj, key) {
return obj == null ? obj : obj[key];
}, window.mesh);
And then check its existence and use the last item in arr to do the assignment.
o && o[arr.pop()] = 12.5;
All of this can be abstracted away into a function that does one or the other based on how many arguments were passed.
function setFromArray(obj, arr, val) {
var keys = arguments.length < 3 ? arr.slice() : arr.slice(0, -1);
var o = keys.slice(0,-1).reduce(function(obj, key) {
return obj == null ? obj : obj[key];
}, window.mesh);
if (arguments.length < 3)
return o;
else
o && o[keys.pop()];
}

Here's a general solution:
function setPropertyPath(obj, path, value) {
var o = obj;
for (var i = 0; i < path.length - 1; i++) {
o = o[path[i]];
}
o[path[path.length - 1]] = value;
}
Usage:
var obj = { a: { b: { c: 0 } } };
setPropertyPath(obj, ['a', 'b', 'c'], 10);
console.log(obj.a.b.c); // prints '10'
JSBin

var mesh = {},
arr = ['options','range','x'],
value = 12.5;
mesh[arr[0]][arr[1]][arr[2]] = value;

If array length is static do something like this:
mesh[array[0]][array[1]][array[2]] = value;
However, one problem with this is that javascript doesn't do autovivification, so if you're accessing a key value that isn't previously defined you could run into errors (if mesh.options hasn't been defined then the above will throw an error because you can't assign to it). To solve that you might abstract this out into a function that handles things recursively:
http://jsfiddle.net/h4jVg/
function update_val(obj, array, val, prev) {
if (array.length == 0) {
obj = val;
return;
}
var cur = array.shift();
if(array.length == 0) {
obj[cur] = val;
return;
} else if (obj[cur] == undefined) {
obj[cur] = {};
}
update_val(obj[cur], array, val);
}

Related

How to MAP an Object Deeply

I want to change somthing in my all values of object let say for example
var a = {a:{c:1},b:2};
desire output : a = {a:{c:5},b:10} //multiply by 5
I am doing like this
var m = function n (o){
return Object.keys(o).map(function(v,i){
if(o[v] !== null && typeof o[v] === 'object') return n(o[v])
else return (o[v]*5);
})
}
a = m({a:{c:1},b:2})
But getting output of a
[
[
5
],
10
]
To generate one object from another, people will frequently use reduce:
var a = {a: {c: 1}, b: 2};
var m = function n(o) {
return Object.keys(o).reduce(function(newObj, key) {
var value = o[key];
if (value !== null && typeof value === 'object') {
newObj[key] = n(value);
} else {
newObj[key] = value * 5;
}
return newObj;
}, {});
};
console.log(m(a));
We pass in a new object as the "accumulator" and set properties on it, returning it from the reduce callback each time so it passes through the entire process.
This is sometimes called an "abusage" of reduce, since the value of the accumulator never changes (it's always the same object; the object's state changes, but not the object), whereas normally the reduce callback alters the value passing through it.
If you prefer, forEach closing over the new object is the other common way:
var a = {a: {c: 1}, b: 2};
var m = function n(o) {
var newObj = {};
Object.keys(o).forEach(function(key) {
var value = o[key];
if (value !== null && typeof value === 'object') {
newObj[key] = n(value);
} else {
newObj[key] = value * 5;
}
});
return newObj;
};
console.log(m(a));
You can do it recursively:
var myObj = {a:{c:1},b:2};
function changeChild(obj) {
var keys = Object.keys(obj);
keys.forEach(key => {
if (typeof obj[key] === "object") {
changeChild(obj[key])
} else if (typeof obj[key] === "number") {
obj[key] = obj[key] * 5;
}
});
}
changeChild(myObj);
console.log(myObj);
A cleaner way...
var m = function (object) {
for(var key in object) {
if(!!object[key] && typeof object[key] === 'object') {
object[key] = m(object[key]);
} else {
object[key] *= 5;
}
}
return object;
};
Array.map function returns Array..this is the reason...and if you know what to change inside the object, why won't you do it directly?
From my point of view you're doing redundant executions for something really simple.
you're creating an anonymous function, using Object.keys to get a new array (after it iterated your object)
you're using Array.map to create another array from this array and inside it creating (another) anonymous function to iterate the new array, and on every iteration you're creating another 4 conditional branches..

Javascript (sub-) Object access by array of keys

Is it possible to set a Variable in an Object by an Array of Keys?
For example i have this Object:
var obj = {'outer': {'inner': 'value'} };
and want to set the Value selected by an Array of Keys:
var keys = ['outer', 'inner'];
to a new Value 'newValue' in order to get this Result:
obj = {'outer': {'inner': 'newValue'} };
What you can do is iterate over the array of keys, making sure that each key exists and leads to another object, until you reach the last key, which you use to set the new value.
function setVal(obj, keys, value) {
var o = obj,
len = keys.length;
// iterate through all keys, making sure each key exists and points to an object
for (var i = 0; i < len - 1; i++) {
var key = keys[i];
// check if the current key exists and is an object
if (obj.hasOwnProperty(key) && typeof obj[key] === 'object' && obj[key]) {
o = o[key];
} else {
// return false or throw an error cause the key is not an object or is missing.
}
}
// update the value at the last key
o[keys[len - 1]] = value;
}
Here's a running example:
function setVal(obj, keys, value) {
var o = obj,
len = keys.length;
for (var i = 0; i < len - 1; i++) {
var key = keys[i];
if (obj.hasOwnProperty(key) && typeof obj[key] === 'object' && obj[key]) {
o = o[key];
} else {
throw new Error('Key ' + key + ' is not an object or is missing.');
}
}
o[keys[len - 1]] = value;
}
var obj = {
'outer': {
'inner': 'value'
}
};
var validKeys = ['outer', 'inner'];
var invalidKeys = ['outer', 'inner', 'extra'];
console.log('Setting valid keys');
setVal(obj, validKeys, 'new value');
console.log(obj);
console.log('Setting invalid keys');
setVal(obj, invalidKeys, 'new value');
console.log(obj);
If you want to have your method only update existing key values and not set new ones, you can wrap the last statement in setVal using hasOwnProperty:
// if object already has the last key, update its value
if (o.hasOwnProperty(keys[len - 1])) {
o[keys[len - 1]] = value;
} else {
// throw an error or return false since the last key doesn't exist.
}
The key point here is that an object property can be accessed using property name string: obj.prop <=> obj["prop"].
function SetValueByKeyNames(obj, keys, value) {
var target = obj;
for(var i = 0; i < keys.length - 1; i++) {
target = target[keys[i]];
if (!target)
return false;
}
target[keys[keys.length - 1]] = value;
return true;
}
var obj = {'outer': {'inner': 'value'} };
var keys = ['outer', 'inner'];
console.log(obj.outer.inner); // value
SetValueByKeyNames(obj, keys, 'newValue');
console.log(obj.outer.inner); // newValue

Set value in JSON by a path using lodash or underscore

I wanna set the value in a JSON using a path string like this "a.0.b" for a JSON that looks like this:
{
a: [
{
b: 'c'
}
]
}
I came up with this solution but I wonder if there is a simpler way to write this:
function setValue(path, value, json) {
var keys = path.split('.');
_.reduce(keys, function(obj, key, i) {
if (i === keys.length - 1) {
obj[key] = value;
} else {
return obj[key];
}
}, json);
}
so calling setValue('a.0.b', 'd', {a:[{b:'c'}]}) would change the json to {a:[{b:'d'}]}
Here's a solution. I benchmarked the two possible solutions and it seems looping over object and path is faster than using the reduce function. See the JSPerf tests here: http://jsperf.com/set-value-in-json-by-a-path-using-lodash-or-underscore
function setValue(path, val, obj) {
var fields = path.split('.');
var result = obj;
for (var i = 0, n = fields.length; i < n && result !== undefined; i++) {
var field = fields[i];
if (i === n - 1) {
result[field] = val;
} else {
if (typeof result[field] === 'undefined' || !_.isObject(result[field])) {
result[field] = {};
}
result = result[field];
}
}
}

Access JavaScript property case-insensitively?

Assume I have an object:
var obj = {
foo:"bar",
fizz:"buzz"
};
I need to access a property of that object dynamically like so:
var objSetter = function(prop,val){
obj[prop] = val;
}
No problems there, except for that prop needs to be case insensitive in case the property name is passed into the function as, say, Foo instead of foo.
So how can I point to an object's property by name without regard to case? I would like to avoid iterating the entire object if possible.
Try this:
var myObject = { "mIxeDCaSEKeY": "value" };
var searchKey = 'mixedCaseKey';
var asLowercase = searchKey.toLowerCase();
myObject[Object.keys(myObject).find(key => key.toLowerCase() === asLowercase)];
You can alternatively already provide the searchKey in lowercase.
If you want it as a function:
/**
* #param {Object} object
* #param {string} key
* #return {any} value
*/
function getParameterCaseInsensitive(object, key) {
const asLowercase = key.toLowerCase();
return object[Object.keys(object)
.find(k => k.toLowerCase() === asLowercase)
];
}
If the key can't be found, then it'll return undefined, just like normal.
If you need to support older browsers, then you can use filter instead:
function getParameterCaseInsensitive(object, key) {
const asLowercase = key.toLowercase();
return object[Object.keys(object).filter(function(k) {
return k.toLowerCase() === asLowercase;
})[0]];
}
I suggest using the polyfills for Object.keys() and Array.filter() if you need even older support.
Note: If you want to also check non-enumerable keys, use Object.getOwnPropertyNames() instead of Object.keys().
Nerdy Note: This assumes your Object doesn't have a key undefined (eg: const foo = {[undefined]: 'bar'};). That's just weird.
Compare all the properties of obj with prop.
var objSetter = function(prop,val){
prop = (prop + "").toLowerCase();
for(var p in obj){
if(obj.hasOwnProperty(p) && prop == (p+ "").toLowerCase()){
obj[p] = val;
break;
}
}
}
For this, I prefer using the prototype over a standalone function just for ease of use and expressiveness. I just don't like funneling objects into functions if I don't have to.
Also, while the accepted answer works, I wanted a more comprehensive solution for both getting and setting that would behave as much like the native dot notation or bracket notation as possible.
With that in mind, I created a couple prototype functions for setting/getting an object property without regard to case. You have to remember to be VERY responsible when adding to the Object prototype. Especially when using JQuery and other libraries. Object.defineProperty() with enumerable set to false was used specifically to avoid conflict with JQuery. I also didn't bother naming the functions anything that indicates they are case-insensitive, but you certainly could. I like shorter names.
Here's the getter:
Object.defineProperty(Object.prototype, "getProp", {
value: function (prop) {
var key,self = this;
for (key in self) {
if (key.toLowerCase() == prop.toLowerCase()) {
return self[key];
}
}
},
//this keeps jquery happy
enumerable: false
});
Here's the setter:
Object.defineProperty(Object.prototype, "setProp", {
value: function (prop, val) {
var key,self = this;
var found = false;
if (Object.keys(self).length > 0) {
for (key in self) {
if (key.toLowerCase() == prop.toLowerCase()) {
//set existing property
found = true;
self[key] = val;
break;
}
}
}
if (!found) {
//if the property was not found, create it
self[prop] = val;
}
return val;
},
//this keeps jquery happy
enumerable: false
});
Now that we've created those functions, our code is super clean and concise and just works.
Case-insensitive getting:
var obj = {foo: 'bar', camelCase: 'humpy'}
obj.getProp("FOO"); //returns 'bar'
obj.getProp("fOO"); //returns 'bar'
obj.getProp("CAMELCASE"); //returns 'humpy'
obj.getProp("CamelCase"); //returns 'humpy'
Case-insensitive setting:
var obj = {foo: 'bar', camelCase: 'humpy'}
obj.setProp('CAmelCasE', 'super humpy'); //sets prop 'camelCase' to 'super humpy'
obj.setProp('newProp', 'newval'); //creates prop 'newProp' and sets val to 'newval'
obj.setProp('NewProp', 'anotherval'); //sets prop 'newProp' to 'anotherval'
Yet another variation on those already presented which pushes the iteration down into the Underscore/Lodash findKey function:
var _ = require('underscore');
var getProp = function (obj, name) {
var realName = _.findKey(obj, function (value, key) {
return key.toLowerCase() === name.toLowerCase();
});
return obj[realName];
};
For example:
var obj = { aa: 1, bB: 2, Cc: 3, DD: 4 };
getProp(obj, 'aa'); // 1
getProp(obj, 'AA'); // 1
getProp(obj, 'bb'); // 2
getProp(obj, 'BB'); // 2
getProp(obj, 'cc'); // 3
getProp(obj, 'CC'); // 3
getProp(obj, 'dd'); // 4
getProp(obj, 'DD'); // 4
getProp(obj, 'EE'); // undefined
This answer requires ES6.
const x = { 'aB': 1, 'X-Total-Count': 10, y3: 2 }
console.log(x[Object.keys(x).find(key=>{return key.match(/^ab$/i)})])
console.log(x[Object.keys(x).find(key=>{return key.match(/^x-total-count$/i)})])
console.log(x[Object.keys(x).find(key=>{return key.match(/^y3$/i)})])
It seems to me like a good candidate for Proxy with traps to convert string keys to either upper case or lower case and behaving like a regular object.
This works with either notation: dots or braquets
Here is the code:
'use strict';
function noCasePropObj(obj)
{
var handler =
{
get: function(target, key)
{
//console.log("key: " + key.toString());
if (typeof key == "string")
{
var uKey = key.toUpperCase();
if ((key != uKey) && (key in target))
return target[key];
return target[uKey];
}
return target[key];
},
set: function(target, key, value)
{
if (typeof key == "string")
{
var uKey = key.toUpperCase();
if ((key != uKey) && (key in target))
target[key] = value;
target[uKey] = value;
}
else
target[key] = value;
},
deleteProperty: function(target, key)
{
if (typeof key == "string")
{
var uKey = key.toUpperCase();
if ((key != uKey) && (key in target))
delete target[key];
if (uKey in target)
delete target[uKey];
}
else
delete target[key];
},
};
function checkAtomic(value)
{
if (typeof value == "object")
return new noCasePropObj(value); // recursive call only for Objects
return value;
}
var newObj;
if (typeof obj == "object")
{
newObj = new Proxy({}, handler);
// traverse the Original object converting string keys to upper case
for (var key in obj)
{
if (typeof key == "string")
{
var objKey = key.toUpperCase();
if (!(key in newObj))
newObj[objKey] = checkAtomic(obj[key]);
}
}
}
else if (Array.isArray(obj))
{
// in an array of objects convert to upper case string keys within each row
newObj = new Array();
for (var i = 0; i < obj.length; i++)
newObj[i] = checkAtomic(obj[i]);
}
return newObj; // object with upper cased keys
}
// Use Sample:
var b = {Name: "Enrique", last: "Alamo", AdDrEsS: {Street: "1233 Main Street", CITY: "Somewhere", zip: 33333}};
console.log("Original: " + JSON.stringify(b)); // Original: {"Name":"Enrique","last":"Alamo","AdDrEsS":{"Street":"1233 Main Street","CITY":"Somewhere","zip":33333}}
var t = noCasePropObj(b);
console.log(JSON.stringify(t)); // {"NAME":"Enrique","LAST":"Alamo","ADDRESS":{"STREET":"1233 Main Street","CITY":"Somewhere","ZIP":33333}}
console.log('.NaMe:' + t.NaMe); // .NaMe:Enrique
console.log('["naME"]:' + t["naME"]); // ["naME"]:Enrique
console.log('.ADDreSS["CitY"]:' + t.ADDreSS["CitY"]); // .ADDreSS["CitY"]:Somewhere
console.log('check:' + JSON.stringify(Object.getOwnPropertyNames(t))); // check:["NAME","LAST","ADDRESS"]
console.log('check2:' + JSON.stringify(Object.getOwnPropertyNames(t['AddresS']))); // check2:["STREET","CITY","ZIP"]
You could do this in order to "normalize" prop
var normalizedProp = prop.toLowerCase();
obj[normalizedProp] = val;
const getPropertyNoCase = (obj, prop) => obj[Object.keys(obj).find(key => key.toLowerCase() === prop.toLowerCase() )];
or
const getPropertyNoCase = (obj, prop) => {
const lowerProp = prop.toLowerCase(obj[Object.keys(obj).find(key => key.toLowerCase() === prop.toLowerCase() )];
}
The ES6 example posted by #nilloc is incorrect and will break in use.
Here is a working example:
const x = {'first':5,'X-Total-Count':10,'third':20};
console.log(x[Object.keys(x).reduce((result,key)=>{
if (!result) {
return key.match(/x-total-count/i)
} else {
return result;
}
},null)]);
or better yet, it should return undefined if the key doesn't exist:
const x = {'first':5,'X-Total-Count':10,'third':20};
console.log(x[Object.keys(x).reduce((result,key)=>{
if (!result) {
return key.match(/x-total-count/i) || undefined
} else {
return result;
}
},undefined)]);
One consideration is that the above example will return the last matching key in the object if there are multiple keys that match.
Here is an example with the code made into a function:
/**
* #param {Object} object
* #param {string} key
* #return {string||undefined} value || undefined
*/
function getKeyCase(obj,key) {
const re = new RegExp(key,"i");
return Object.keys(obj).reduce((result,key)=>{
if (!result) {
return key.match(re) || undefined
} else {
return result;
}
},undefined);
const x = {'first':5,'X-Total-Count':10,'third':20};
console.log(x[getKeyCase(x,"x-total-count")]);
Its really sad that the iteration can't be skipped as it seems. For me what is acceptable but may not be for everyone is to shape the object one time via iteration and then use it in regular hashmap fashion.
const hashmap = {
'FOO': 'foo as in function programming',
'bar': 'bar is in baz',
};
const shapedmap = Object.entries(hashmap).reduce(
(acc, [key, val]) => (acc[key.toUpperCase()] = val, acc), {}
);
for (const term of ['foo', 'bar', 'baz']) {
const match = shapedmap[term.toUpperCase()]
match && console.log('awesome, we got the term.', match);
};
Even if it just one time lookup has to be performed, it shouldn't less performant as any other iteration solution since after 1 pass, the lookup speed is constant. (I guess).
This is an old question, but it was the first one I found.
As #ZachSmith says, you can use a Proxy.
Here's some example code:
function lowercase(oldKey) {
// Check that it's a string.
return typeof oldKey === 'string' ? oldKey.toLowerCase() : oldKey;
}
const propertiesMap = new Map(
Object.keys(obj).map(propKey => [lowercase(propKey), obj[propKey]])
);
const caseInsensitiveGetHandler = {
get: function(target, property, receiver) {
return propertiesMap.get(lowercase(property));
}
};
obj = new Proxy(obj, caseInsensitiveGetHandler);
For my use case, I only needed to proxy the object's getter, but you may need to implement more of the Proxy methods.
There is no need for any iteration. Since prop might not be a string, it should be coerced to a string first where appropriate since that's what objects do natively. A simple getter function is:
function objGetter(prop) {
return obj[String(prop).toLowerCase()];
}
If there is a requirement is to restring access to own properties:
function objGetter(prop) {
prop = String(prop).toLowerCase();
if (obj.hasOwnProperty(prop)) {
return obj.prop;
}
}
and a setter:
function objSetter(prop, val) {
obj[String(prop).toLowerCase()] = val;
}
Heres a very simple code to do this
Assuming that data is the array of objects like
data=[{"A":"bc","B":"nn"}]
var data=data.reduce(function(prev, curr) {
var cc = curr; // current value
var K = Object.keys(cc); // get all keys
var n = {};
for (var i = 0; i < K.length; i++) {
var key = K[i];//get hte key
n[key.toLowerCase()] = cc[key] // convert to lowercase and assign
}
prev.push(n) // push to array
return prev;
}, [])
Output will be
data=[{"a":"bc","b":"nn"}]
You might only need to do case-insensitive matching (usually expensive because of object iteration) IF a case-sensitive match (cheap and quick) fails.
Say you have:
var your_object = { "Chicago" : 'hi' , "deTroiT" : 'word' , "atlanta" : 'get r dun' } ;
And you have, for whatever reason, the_value, Detroit:
if( your_object.hasOwnProperty( the_value ) )
{
// do what you need to do here
}
else
{ // since the case-sensitive match did not succeed,
// ... Now try a the more-expensive case-insensitive matching
for( let lvs_prop in your_object )
{ if( the_value.toLowerCase() == lvs_prop.toLowerCase() )
{
// do what you need to do here
break ;
} ;
}
} ;
why would we do it that complicated when we simply can make it all lower case:
var your_object = {
"chickago" : 'hi' ,
"detroit" : 'word',
"atlanta" : 'get r dun',
GetName: function (status) {
return this[status].name;
} };
to call it: your_object.GetName(your_var.toLowerCase());
Another simple way:
function getVal(obj, prop){
var val;
prop = (prop + "").toLowerCase();
for(var p in obj){
if(obj.hasOwnProperty(p) && prop == (p+ "").toLowerCase()){
val = obj[p]
break;
}
}
return val;
}
Use it like this:
var obj = {
foo:"bar",
fizz:"buzz"
};
getVal(obj,"FoO") -> returns "bar"
Here is a nice recursive function that allows you to traverse a javascript object in a case-insensitive way:
let testObject = {'a': {'B': {'cC': [1,2,3]}}}
let testSeq = ['a','b','cc']
function keySequence(o, kseq) {
if(kseq.length==0){ return o; }
let validKeys = Object.keys(o).filter(k=>k.toLowerCase()==kseq[0].toLowerCase());
if(validKeys.length==0) { return `Incorrect Key: ${kseq[0]}` }
return keySequence(o[validKeys[0]], kseq.slice(1))
}
keySequence(testObject, testSeq); //returns [1,2,3]
This will convert everything to lowercase, but in a bind this could help if you are not concerned with retaining case.
var somedata = {
"MixEdCase": 1234
}
var temp = JSON.parse(JSON.stringify(somedata).toLowerCase());
console.log(temp.mixedcase);
// or
console.log(temp["mixedcase"]);
So, you will need to get the object key that matches the case of the existing object, then use this to do your object update.
const obj = {
foo:"bar",
fizz:"buzz"
};
// to get obj.foo or obj.FOO or obj.foO returning "bar"
// create regex expression of case insensitive version of the key string
const regex=passedKey=> new RegExp(`^${passedKey}$`,'gi');
// find the key that matches the string you are passing
const formattedKey=passedKey=>Object.keys(obj).find(key=>regex(passedKey).test(key));
formattedKey('Foo'); // returns foo
formattedKey('FoO'); // returns foo
// consequently you can can use it like wise
obj[formattedKey('Foo')] // returns bar
obj[formattedKey('FoO')] // returns bar
obj[formattedKey('foo')] // returns bar

test the existence of property in a deep object structure

In javascript, lets say I want to access a property deep in an object, for example:
entry.mediaGroup[0].contents[0].url
At any point along that structure, a property may be undefined (so mediaGroup may not be set).
What is a simple way to say:
if( entry.mediaGroup[0].contents[0].url ){
console.log( entry.mediaGroup[0].contents[0].url )
}
without generating an error? This way will generate an undefined error if any point along the way is undefined.
My solution
if(entry) && (entry.mediaGroup) && (entry.MediaGroup[0]) ...snip...){
console.log(entry.mediaGroup[0].contents[0].url)
}
which is pretty lengthy. I am guessing there must be something more elegant.
This is a very lazy way to do it, but it meets the criteria for many similar situations:
try {
console.log(entry.mediaGroup[0].contents[0].url);
} catch (e) {}
This should not be done on long code blocks where other errors may potentially be ignored, but should be suitable for a simple situation like this.
/*decend through an object tree to a specified node, and return it.
If node is unreachable, return undefined. This should also work with arrays in the tree.
Examples:
var test1 = {a:{b:{c:{d:1}}}};
console.log(objectDesend(test1, 'a', 'b', 'c', 'd'));
var test2 = {a:{b:{c:1}}}; //will fail to reach d
console.log(objectDesend(test2, 'a', 'b', 'c', 'd'));
*/
var objectDescend = function(){
var obj = arguments[0];
var keys = arguments;
var cur = obj;
for(var i=1; i<keys.length; i++){
var key = keys[i];
var cur = cur[key];
if(typeof(cur)=='undefined')
return cur;
}
return cur;
}
var test1 = {a:{b:{c:{d:1}}}};
console.log(objectDescend(test1, 'a', 'b', 'c', 'd'));
var test2 = {a:{b:{c:1}}};
console.log(objectDescend(test2, 'a', 'b', 'c', 'd'));
So this will return either the value you are looking for, or undefined since that value doesn't exist. It won't return false, as that may actually be the value you are looking for (d:false).
In my code base, I add Object.prototype.descend, so I can do test1.descend('a', 'b', 'c', 'd'). This will only work in ECMAScript 5 (IE>=9) since you need to make it so your function doesn't appear in enumerations. For more info:
Add a method to Object primative, but not have it come up as a property
Here is my code for that:
Object.defineProperty(Object.prototype, 'descend', {
value: function(){
var keys = arguments;
var cur = this;
for(var i=0; i<keys.length; i++){
var key = keys[i];
var cur = cur[key];
if(typeof(cur)=='undefined')
return cur;
}
return cur;
}
});
var test1 = {a:{b:{c:{d:false}}}};
//this will return false, which is the value of d
console.log(test1.descend('a', 'b', 'c', 'd'));
var test2 = {a:{b:{c:1}}};
//undefined since we can't reach d.
console.log(test2.descend(test2, 'a', 'b', 'c', 'd'));
Your current solution is probably as good as you can get, as mVChr says, try..catch is just lazy here. It's probably far less effient and has nothing to recommend it other than perhaps being easier to type (but not significantly so) and it'll be harder to debug as it silently hides errors.
The real issue is the very long "reference worm" created by attempting such access. An alternative to the original that at least reduces the number of property lookups is:
var o;
if ( (o = entry ) &&
(o = o.mediaGroup) &&
(o = o[0] ) &&
(o = o.contents ) &&
(o = o[0] )) {
alert(o.url);
}
But I expect you won't like that.
If you have many such deep access paths, you might like to create a function to do the access and return the last object on success or some other vaule on failure. For failure, you could also have it return the last non-falsey object on the path.
// Create test object
var entry = {};
entry.mediaGroup = [{
contents: [{url: 'url'}]
}];
// Check that it "works"
// alert(entry.mediaGroup[0].contents[0].url);
// Deep property access function, returns last object
// or false
function deepAccess(obj) {
var path = arguments;
var i = 0, iLen = path.length;
var o = path[i++]; // o is first arg
var p = path[i++]; // p is second arg
// Go along path until o[p] is falsey
while (o[p]) {
o = o[p];
p = path[i++];
}
// Return false if didn't get all the way along
// the path or the last non-falsey value referenced
return (--i == iLen) && o;
}
// Test it
var x = deepAccess(entry, 'mediaGroup','0','contents','0');
alert(x && x.url); // url
var x = deepAccess(entry, 'mediaGroup','1','contents','0');
alert(x && x.url); // false
There are probably 3-4 different questions along this vein, and four times as many answers. None of them really satisfied me, so I made my own, and I'll share it.
This function is called "deepGet".
Example:
deepGet(mySampleData, "foo.bar[2].baz", null);
Here is the full code:
function deepGet (obj, path, defaultValue) {
// Split the path into components
var a = path.split('.');
// If we have just one component left, note that for later.
var last = (a.length) === 1;
// See if the next item is an array with an index
var myregexp = /([a-zA-Z]+)(\[(\d+)\])+/; // matches: item[0]
var match = myregexp.exec(a[0]);
// Get the next item
var next;
if (match !== null) {
next = obj[match[1]];
if (next !== undefined) {
next = next[match[3]];
}
} else {
next = obj[a[0]];
}
if (next === undefined || next === null) {
// If we don't have what we want, return the default value
return defaultValue;
} else {
if (last) {
// If it's the last item in the path, return it
return next;
} else {
// If we have more items in the path to go, recurse
return deepGet (next, a.slice(1).join("."), defaultValue);
}
}
}
Here is a jsFiddle: http://jsfiddle.net/7quzmjh8/2/
I was inspired by these two things:
http://designpepper.com/blog/drips/making-deep-property-access-safe-in-javascript.html
http://jsfiddle.net/wxrzM/1/
Hopefully this is useful to someone out there :)
I use this simple function for playing around with deep object properties:
getProperty = function(path) {
try {
return eval(path);
}
catch (e) {
return undefined;
}
};
Here's an example:
var test = {a:{b:{c:"success!"}}};
alert(getProperty('test.c.c'));
// undefined
alert(getProperty('test.a.b.c'));
// success!
Here's the one i have been using for a while
var obj = { a: { b: [
{ c: {d: 'XYZ'} }
] } };
// working
obj.a.b[0].c.d = null;
console.log('value:'+getProperty(obj, 'a.b[0].c.d', 'NOT-AVAILABLE')); // value:null
obj.a.b[0].c.d = 'XYZ';
console.log('value:'+getProperty(obj, 'a.b[0].c.d', 'NOT-AVAILABLE')); // value:XYZ
console.log('value:'+getProperty(obj, 'a.b[0].c.d.k.sds', 'NOT-AVAILABLE')); // value:NOT-AVAILABLE
obj.a.b[0].c = null;
console.log('value:'+getProperty(obj, 'a.b[0].c.d', 'NOT-AVAILABLE')); // value:NOT-AVAILABLE
// will not work
//console.log('v:'+getProperty(obj, 'a.b["0"].c.d'));
Here's the function
function getProperty(obj, str, defaultValue){
var props = str.split('.').map(function(prop){
var arrAccessRegEx = /(.*)\[(.*)\]/g;
if (arrAccessRegEx.test(prop)){
return prop.split(arrAccessRegEx).filter(function(ele){return ele!=''; });
} else {
var retArr = [];
retArr.push(prop);
return retArr
};
});
//console.log(props);
for(var i=0;i<props.length;i++){
var prop = props[i][0];
//console.log('prop:'+prop);
if (obj === null) return defaultValue;
obj = obj[prop];
if (obj === undefined) return defaultValue;
if (props[i].length == 2){
var idx = props[i][1];
if (!(obj instanceof Array)) return defaultValue;
if (idx < obj.length ){
obj = obj[idx];
if (obj === undefined) return defaultValue;
}
}
} // for each item in split
return obj;
}

Categories

Resources