2
\$\begingroup\$

I have an object of arbitrary depth, and a string (or array) containing the keys which have to be traversed to get to the value.

As far as I know, there's no method to get to the value directly:

var object = { 
    user: {name: 'bob'}, 
    'user.name': 'error'
};

object['user.name']; // contains 'error'

And the usual:

object['user']['name'];

is not possible without doing some very dirty eval stuff, because you do not know how far you will need to traverse through the object beforehand.

So I think this is only solvable using a recursive function:

var object = {
    company: {
        users: [
            {name:'alice',age:'20'},
            {name:'bob',age:'22'},
            {name:'eve',age:'30'},
        ]
    }
};

lookup = function recurse(array,object) {
    var next = (array.length) ? object[array.shift()] : object;
    return (next instanceof Object && next[array[0]]) ? recurse(array,next) : next;
}

var search = 'company.users.0.name';
lookup(search.split('.'), object);

I think this works well enough, but would love some feedback.

Efficiency is important here, could I rewrite the function to shave of a few milliseconds somewhere? Are there any pitfalls, bugs or exceptional cases to consider?

\$\endgroup\$
12
  • 1
    \$\begingroup\$ Welcome to Code Review! You say efficiency is important. Just in CPU or also in memory? And have you tried how fast/efficient your current code is (in other words, have you tried a profiler)? \$\endgroup\$
    – Mast
    Commented Apr 12, 2016 at 10:20
  • 2
    \$\begingroup\$ I can't really understand what's your need. Especially when looking at your loopup() function: merely use object.company.users[0].name directly gives the same result! In the other hand, I don't see how it's related to the example you first show at the beginning of the question: what's the matter with the strange 'user.name': 'error' member? Sorry if I missed something obvious, but could you clarify? \$\endgroup\$
    – cFreed
    Commented Apr 12, 2016 at 17:28
  • 1
    \$\begingroup\$ Any reason you don't want to use eval? function lookup(object, search) { try { return eval("object['" + search.replace(/\./g, "']['") + "']"); } catch(e) { return null; } } \$\endgroup\$ Commented Apr 12, 2016 at 19:32
  • 1
    \$\begingroup\$ And what about situations where an object's key could have a period? var a = {}; a["happy.birthday"] = true;? \$\endgroup\$ Commented Apr 12, 2016 at 19:37
  • 1
    \$\begingroup\$ Thanks for the clarification: now I understand. Please look at my answer. \$\endgroup\$
    – cFreed
    Commented Apr 14, 2016 at 12:55

2 Answers 2

4
\$\begingroup\$

The very first problem I see is:

next instanceof Object

That's because almost everything, if not everything, in JS is an instance of Object. Go try new Date() instanceof Object and even that is true. That said, you can do it this way but you must assume that it's just a structure containing objects and arrays. Otherwise, you'll have to check for every possible kind of object out there. This means dates, regexes, DOM objects, functions etc.

Another way to do this is to create a function dynamically. I've seen frameworks do this, especially when it deals with templates containing JS expressions. Instead of lugging around a full-blown expression parser, they let the browser's parser do it. The advantage of this method over eval is that you only evaluate the expression once. You can then cache the generated function in some object for reuse. You can even have it return undefined or null if the entire subtree is missing instead of getting an error.

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);

// Even when the subtree is missing, you can safely call it
var obj2 = { foo: {}};
var resolver2 = createResolver('foo.bar.baz');
var value2 = resolver2(obj2);

// Normally, this throws an error:
var value3 = obj2.foo.bar.baz
\$\endgroup\$
4
  • \$\begingroup\$ Thanks for the input! The objects only contain objects, arrays, strings, integers and null. While typeof null == 'object' is true, null instanceof Object returns false, so no problems there either. \$\endgroup\$
    – okdewit
    Commented Apr 13, 2016 at 13:34
  • \$\begingroup\$ The dynamical function generator is very interesting, it looks very clean. But am I correct in thinking that it would be a bit more difficult to implement for browsers not supporting the ES6 template literals (string interpolation)? \$\endgroup\$
    – okdewit
    Commented Apr 13, 2016 at 13:42
  • 1
    \$\begingroup\$ @Fx32 Yep, it's possible on older browsers, just not that pretty. \$\endgroup\$
    – Joseph
    Commented Apr 13, 2016 at 14:48
  • 1
    \$\begingroup\$ @JosephtheDreamer this is beautiful \$\endgroup\$ Commented Apr 14, 2016 at 13:32
3
\$\begingroup\$

Indeed the answer given by @Joseph the Dreamer is quite interesting, and made me perceive some possibilities I never thought yet.

But for the current question it fails when the object includes arrays, as illustrated here:

var object = {
    company: {
        users: [
            {name:'alice',age:'20'},
            {name:'bob',age:'22'},
            {name:'eve',age:'30'},
        ]
    }
};
          
function createResolver(keypath){
  return new Function('root', `
    try { return root.${keypath}; }
    catch(e){ return undefined; }
 `);
}

var resolver3 = createResolver('company.users.0.name');
console.log(resolver3(object));

So the approach already proposed by the OP seems inescapable: work step by step, in order to distinguishly process each key part.
This way, object[keypart] works fine regardless keypart is really a key name or an index value (thanks to automatic cast).

Here I only propose a simpler way to do the same, working fine in any circumstance (including a falsy keypath):

var object = {
    company: {
        users: [
            {name:'alice',age:'20'},
            {name:'bob',age:'22'},
            {name:'eve',age:'30'},
        ]
    }
};

function resolve(keys, obj) {
  return keys.split('.').reduce(function(prev, curr) {
    return prev ? prev = prev[curr] : undefined;
  }, obj);
}

console.log(resolve('company.users.0.name', object));
console.log(resolve('company.foo.0.name', object)); // undefined
console.log(resolve('company.users.5.name', object)); // undefined

\$\endgroup\$
1
  • \$\begingroup\$ Oh wow, never thought about using reduce, even though it's perfect for cases like these. Looks very nice! \$\endgroup\$
    – okdewit
    Commented Apr 15, 2016 at 9:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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