/**
* Returns the value of a nested property of source object if it exists.
* Even if the nested property evaluates to false it will be returned.
* If the nested property does not exist, undefined will be returned.
* @param {Object} source source object
* @param {String} path path without first dot e.g. "response.http.statusCode"
* @param {String=} sourceName if provided, any catched error will be logged to console
* @returns {*} value under the source.path or undefined
*/
function reach(source, path, sourceName) {
var result;
try {
if (sourceName) {
eval('var ' + sourceName + '=source; result=' + sourceName + '.' + path);
} else {
eval('result=source.' + path);
}
} catch (err) {
if (sourceName) {
console.log(err);
}
}
return result;
}
I know that eval
is dangerous, but I made the assumption that it's the caller's responsibility.
2 Answers 2
Very similar to an earlier answer I did with regards to deep value extraction. Here's the code:
function createResolver(keypath){
return new Function('root', `
try { return root.${keypath}; }
catch(e){ return undefined; }
`);
}
// Return foo.bar.baz
var obj1 = { foo: { bar: { baz: 'bam!!!' }}};
var resolver1 = createResolver('foo.bar.baz');
var value1 = resolver1(obj1);
One difference between this one and your approach is that your approach seems to evaluate on every call. This can be slow as you are having the browser evaluate the string JavaScript on every call. A constructed function on the other hand can be cached. This means you can construct the function once, store the constructed function, and call it over and over again without having to re-evaluate the string every time.
// keypath-resolver store
var resolvers = {}
function createResolver(keypath){
return new Function('root', `
try { return root.${keypath}; }
catch(e){ return undefined; }
`);
}
// Create a resolver for foo.bar.baz. We only have JS evaluate the string here.
resolvers['foo.bar.baz'] = createResolver('foo.bar.baz');
var obj1 = { foo: { bar: { baz: 'bam!!!' }}};
var obj2 = { foo: { bar: { baz: 'pow!!!' }}};
// We just call the function. No evaluation!
resolvers['foo.bar.baz'](obj1);
resolvers['foo.bar.baz'](obj2);
Although these two approaches seem to act the same, one issue with eval
is that it evaluates the string in the scope it is called. This means it can leak variables or override code that's visible in the current scope, just like you did with result
. A generated function on the other hand does not. It does not form a closure of the scope it was created from, and the immediate outer scope is the global scope. This makes it relatively safer than eval
.
Your reliance of console.log
is also something to ponder on. If it's an error, is it not better to actually throw than just log the error? Also, if the keypath isn't fully resolved, this means the value isn't present which makes it technically undefined
. Consider returning undefined
instead of logging an error if you want errors to be handled.
-
\$\begingroup\$ This implementation is very good. I created a small benchmark \$\endgroup\$Jan Grz– Jan Grz2016年04月19日 18:03:11 +00:00Commented Apr 19, 2016 at 18:03
-
\$\begingroup\$ My implementation:
reach-deep for existing property invoked 100000 times: 46.194ms
reach-deep for existing property invoked 100000 times: 52.614ms
\$\endgroup\$Jan Grz– Jan Grz2016年04月19日 18:13:27 +00:00Commented Apr 19, 2016 at 18:13 -
\$\begingroup\$ @josephSilber 's implementation:
reachAlternative for existing property invoked 100000 times: 143.837ms
reachAlternative for non-existing property invoked 100000 times: 164.550ms
\$\endgroup\$Jan Grz– Jan Grz2016年04月19日 18:13:38 +00:00Commented Apr 19, 2016 at 18:13 -
\$\begingroup\$ Popular npm package that solves this problem:
reach for existing property invoked 100000 times: 271.807ms
reach for non-existing property invoked 100000 times: 271.707ms
\$\endgroup\$Jan Grz– Jan Grz2016年04月19日 18:13:47 +00:00Commented Apr 19, 2016 at 18:13 -
\$\begingroup\$ Joseph the Dreamer's implementation
resolver for existing property invoked 100000 times: 4.460ms
resolver for non-existing property invoked 100000 times: 14.469ms
\$\endgroup\$Jan Grz– Jan Grz2016年04月19日 18:13:57 +00:00Commented Apr 19, 2016 at 18:13
There are a few things I would improve:
Don't use
eval
, even if you think that in this particular case it's safe. Even if it were, it's still slow, and not supported everywhere JS is.There's no point in using this function if you know that the path exists. The whole point is to try to fetch something through a deep path without an error. I think you can skip the console logging bit.
It'd make sense to allow the user to provide a default value to be returned if the final path segment is not found.
If you're doing this, you may as well support arrays too.
In light of the above, here's what I'd do:
function reach (source, path, replacement) {
var key;
if ( ! Array.isArray(path)) {
path = path.split('.');
}
while (key = path.shift()) {
if (Array.isArray(source)) {
if (isNaN(key) || typeof source[key] === 'undefined') {
return replacement;
}
source = source[key];
} else if (source && source.hasOwnProperty(key)) {
source = source[key];
} else {
return replacement;
}
}
return source;
}
const source = {
build: {
house: {
types: ['Stone', 'Bricks']
}
}
};
reach(source , 'build.house') // { types: ['Stone', 'Bricks'] }
reach(source , 'build.house.types') // ['Stone', 'Bricks']
reach(source , 'build.house.types.length') // undefined
reach(source , 'build.house.types.1') // 'Bricks'
reach(source , 'build.cot.types.1') // undefined
reach(source , 'build.cot.types.1', 'Wood') // 'Wood'
-
\$\begingroup\$ Thiw would not work for objects that are hidden inside a function. \$\endgroup\$Jan Grz– Jan Grz2016年04月19日 16:39:18 +00:00Commented Apr 19, 2016 at 16:39
-
\$\begingroup\$ Also this implementation is about 3 times slower for existing paths, and 30% slower for non existing. \$\endgroup\$Jan Grz– Jan Grz2016年04月19日 16:40:06 +00:00Commented Apr 19, 2016 at 16:40
-
\$\begingroup\$ @JanOsch - what do you mean by "hidden inside a function"? \$\endgroup\$Joseph Silber– Joseph Silber2016年04月19日 16:43:29 +00:00Commented Apr 19, 2016 at 16:43
-
\$\begingroup\$ As for the speed: if that's your main concern, you could probably speed this thing up significantly if you replace
while (key = path.shift())
with afor
loop. Messing with the array like that is pretty expensive. \$\endgroup\$Joseph Silber– Joseph Silber2016年04月19日 16:44:48 +00:00Commented Apr 19, 2016 at 16:44 -
\$\begingroup\$ I checked the implementation with a
for(;;)
loop, and it's still more than 2 times slower than my original. \$\endgroup\$Jan Grz– Jan Grz2016年04月19日 18:21:54 +00:00Commented Apr 19, 2016 at 18:21
response.http.statusCode
in a string variable and not something you can just code directly? \$\endgroup\$let code = response.http.statusCode
I would get the familiarcannot read property "http" of undefined
exception. \$\endgroup\$