2
\$\begingroup\$
/**
 * 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.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ I'm unclear why this is needed or useful or what problem you're trying to solve? Under what circumstance is a path like response.http.statusCode in a string variable and not something you can just code directly? \$\endgroup\$ – jfriend00 Apr 18 '16 at 21:54
  • \$\begingroup\$ Are you writing a compiler or something? \$\endgroup\$ – Naftali aka Neal Apr 18 '16 at 21:58
  • \$\begingroup\$ @Jfriend I if response does not exist, by calling let code = response.http.statusCode I would get the familiar cannot read property "http" of undefined exception. \$\endgroup\$ – Jan Grz Apr 19 '16 at 18:27
1
\$\begingroup\$

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.

\$\endgroup\$
6
  • \$\begingroup\$ This implementation is very good. I created a small benchmark \$\endgroup\$ – Jan Grz Apr 19 '16 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 Apr 19 '16 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 Apr 19 '16 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 Apr 19 '16 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 Apr 19 '16 at 18:13
0
\$\begingroup\$

There are a few things I would improve:

  1. 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.

  2. 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.

  3. It'd make sense to allow the user to provide a default value to be returned if the final path segment is not found.

  4. 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;
}

Here's how to use it:

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'
\$\endgroup\$
6
  • \$\begingroup\$ Thiw would not work for objects that are hidden inside a function. \$\endgroup\$ – Jan Grz Apr 19 '16 at 16:39
  • \$\begingroup\$ Also this implementation is about 3 times slower for existing paths, and 30% slower for non existing. \$\endgroup\$ – Jan Grz Apr 19 '16 at 16:40
  • \$\begingroup\$ @JanOsch - what do you mean by "hidden inside a function"? \$\endgroup\$ – Joseph Silber Apr 19 '16 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 a for loop. Messing with the array like that is pretty expensive. \$\endgroup\$ – Joseph Silber Apr 19 '16 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 Apr 19 '16 at 18:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.