5
\$\begingroup\$

I've made a function to safely traverse deep objects.

var foo = {
    bar: {
        baz: 1
    }
};

function getDeepProp(obj, properties) {

    if (typeof properties === 'string')
        properties = properties.split('.');

    if (typeof obj === 'undefined')
        return;

    if (!properties.length)
        return obj;

    return getDeepProp(obj[properties[0]], properties.slice(1));
}

console.log(getDeepProp(foo, 'bar.baz')); // 1
console.log(getDeepProp(foo, 'baz.baz')); // undefined

// The function was created to avoid exceptions like:
console.log(foo.baz.baz); // Uncaught TypeError: Cannot read property 'baz' of undefined    

Please suggest possible improvements.

http://jsfiddle.net/rtkczasa/

\$\endgroup\$
8
\$\begingroup\$

It is a good exercise on recursion, but I'd suggest using built-in functions. Think that what you want to abstract is this syntax:

obj[a][b][c]

But doing some checking in between:

obj[a] && obj[a][b] && obj[a][b][c]

It seems to be a good job for a reducer, where obj is the accumulator, that gets passed along, and the properties are given as an array that you can reduce over, so p below will be each of the properties above:

function getDeepProp(obj, props) {
  return props.split('.').reduce(function(acc, p) {
    // if the accumulator is something
    // then lookup the next nested property
    // otherwise return undefined
    if (acc == null) return
    return acc[p]
  },obj) // the initial accumulator is the object
}

For example:

var obj = {
  a: {
    b: {
      c: 1000
    }
  }
}

console.log(getDeepProp(obj, 'a.b.c')) //=> 1000
console.log(getDeepProp(obj, 'a.b.foo')) //=> undefined

Note that acc can be an object, or an array, since arrays are objects, and the bracket syntax is the same:

console.log(getDeepProp([[[2,[3]]]], '0.0.1.0')) //=> 3

Also, I would worry about the function doing too much. If properties have not been passed, then the function shouldn't be used in the first place. That's why I'd suggest, among other reasons, to take the object (the receiver of the operation), always last:

function getDeepProp(props, obj)
\$\endgroup\$
  • \$\begingroup\$ Nice, I would have never considered this. \$\endgroup\$ – konijn Dec 10 '14 at 22:04
  • \$\begingroup\$ I really like this solution. Great idea! \$\endgroup\$ – Johan Dec 11 '14 at 8:43
  • \$\begingroup\$ There's only one problem with this one. If obj.a.b is null (or simply "falsy", like false), then calling getDeepProp(obj, 'a.b.c') will also return null (or whatever the falsy value was), even though the c property has no right of existence (should be undefined). Voted up anyway, great and original approach :) \$\endgroup\$ – Michal Leszczyk Dec 11 '14 at 10:16
  • \$\begingroup\$ Or, if the obj.a.b is supposed to be a string and you'd use this function to check it's length property, then an empty string (obj.a.b = "") will return the "" and not the value of length (because an empty string is falsy in JavaScript). \$\endgroup\$ – Michal Leszczyk Dec 11 '14 at 10:25
  • \$\begingroup\$ @MichalLeszczyk, falsy values will work. Try getDeepProp({a:{b:{c:false}}}, 'a.b.c'), or with empty string, it should work. \$\endgroup\$ – elclanrs Dec 11 '14 at 21:36
0
\$\begingroup\$

This is a case where eval is not actually evil, given some constraints.

function getDeepProp(properties) {
    if (!/^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(properties)) {
        throw new Error("Invalid property name: " + properties);
    }

    try {
        return eval(properties);
    }
    catch (error) {
        throw new Error("Failed to get value for " + properties + " (" + error + ")");
    }
}

var foo = {
    bar: {
        baz: "The value you want"
    }
};

console.log(getDeepProp("foo.bar.baz"));      // Logs the value of foo.bar.baz
console.log(getDeepProp("alert('Hacked!');"); // Throws an error

Modified JSFiddle: http://jsfiddle.net/rtkczasa/1/

By using a Regular Expression, you guard against script injection attacks because only valid property names will be passed into eval, not arbitrary JavaScript commands.

In the context of accessing a property value, you must ask yourself this question: What's the difference between foo.bar.baz, foo["bar"]["baz"] (which is what your code essentially does), and eval("foo.bar.baz")?

Answer: There is no difference.

I haven't done a performance benchmark on our two approaches, but I'm thinking you'll get a slight performance boost using eval in this case.

People tend to blindly fear eval and this isn't necessary. The blog post eval() isn’t evil, just misunderstood made me rethink my fear of eval and I believe it has its place --- and this is one of them.

Despite popular theory (and Crockford’s insistence), the mere presence of eval() does not indicate a problem. Using eval() does not automatically open you up to a Cross-Site Scripting (XSS) attack nor does it mean there is some lingering security vulnerability that you’re not aware of. Just like any tool, you need to know how to wield it correctly, but even if you use it incorrectly, the potential for damage is still fairly low and contained.

\$\endgroup\$
  • \$\begingroup\$ I'm not the downvoter, though I have to admit that I don't like eval. If you look at this source github.com/douglascrockford/JSON-js/blob/master/json2.js, you noticed that you need to adjust your regex \$\endgroup\$ – Johan Dec 11 '14 at 8:51
  • \$\begingroup\$ People often blindly fear eval without exploring the true risks. While the regex in my code won't allow foo["bar"], I don't see that as a show stopper. It assumes the property you want to get is a valid property name in JavaScript. \$\endgroup\$ – Greg Burghardt Dec 11 '14 at 15:09

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.