138

I'm temporarily stuck with what appears to be a very simple JavaScript problem, but maybe I'm just missing the right search keywords!

Say we have an object

var r = { a:1, b: {b1:11, b2: 99}};

There are several ways to access the 99:

r.b.b2
r['b']['b2']

What I want is to be able to define a string

var s = "b.b2";

and then access the 99 using

r.s or r[s] //(which of course won't work)

One way is to write a function for it that splits the string on dot and maybe recursively/iteratively gets the property. But is there any simpler/more efficient way? Anything useful in any of the jQuery APIs here?

2
  • You could always build a string and eval() it when needed, but I don't think anybody would call that a good idea. Parsing your string the way you described is safer. Commented Nov 8, 2011 at 14:38
  • @jrummell In a struts2 application I'm using a jqgrid that gets a rowObject in a column formatter. The rowObject object structure follows the column data model which contains some nested properties that I need to access in a generic way inside a loop. Commented Nov 8, 2011 at 17:50

13 Answers 13

148

Here's a naive function I wrote a while ago, but it works for basic object properties:

function getDescendantProp(obj, desc) {
    var arr = desc.split(".");
    while(arr.length && (obj = obj[arr.shift()]));
    return obj;
}

console.log(getDescendantProp(r, "b.b2"));
//-> 99

Although there are answers that extend this to "allow" array index access, that's not really necessary as you can just specify numerical indexes using dot notation with this method:

getDescendantProp({ a: [ 1, 2, 3 ] }, 'a.2');
//-> 3
Sign up to request clarification or add additional context in comments.

6 Comments

jshint told me to rewrite: while(arr.length) { obj = obj[arr.shift()]; }
@user1912899: that's not quite the same, but is probably a little more robust in that it will throw errors (whereas mine will just return undefined). It depends on your preference, I suppose. JSHint just complains about the assignment in the loop conditional, which can be disabled using the boss option.
you can do while(arr.length && obj) { obj = obj[arr.shift()]; }
a[1] doesn't work :(
@JuanDavid: no, this isn't like an eval statement, it's just a simple split and loop. You can use a.1, unless you have some properties with a '.' in the name. If that's the case, you'll need a more complex solution.
|
146

split and reduce while passing the object as the initalValue

Update (thanks to comment posted by TeChn4K)

With ES6 syntax, it is even shorter

var r = { a:1, b: {b1:11, b2: 99}};
var s = "b.b2";

var value = s.split('.').reduce((a, b) => a[b], r);

console.log(value);

Old version

var r = { a:1, b: {b1:11, b2: 99}};
var s = "b.b2";

var value = s.split('.').reduce(function(a, b) {
  return a[b];
}, r);

console.log(value);

5 Comments

A small improvement for cases where you might try to lookup a non-existing subvalue of an object i.e.: c.c1. To catch this, simply add a check to the return value like this: return (a != undefined) ? a[b] : a ;
@AmmarCSE would this work if one of the nested properties is array? For example var key = "b.b2[0].c" ?
@AmmarCSE actually it does not work. Would love a tip how could I update your example to support that. Thanks
@PrimozRome, hey there! It can work if you can use b.b2.0.c instead. See jsfiddle.net/8bwmdjnL/1 . If you cant use a . and have to stick with the brackets, you will need to do some more manual work. Let me know! :-)
That is a great and very short solution without using eval, that is fantastic
44

You can use lodash get() and set() methods.

Getting

var object = { 'a': [{ 'b': { 'c': 3 } }] };

_.get(object, 'a[0].b.c');
// → 3

Setting

var object = { 'a': [{ 'b': { 'c': 3 } }] };

_.set(object, 'a[0].b.c', 4);
console.log(object.a[0].b.c);
// → 4

2 Comments

nice, its really simple with lodash
Most of the other answers I see are for setting. This is simple and works for both!
22

If it's possible in your scenario that you could put the entire array variable you're after into a string you could use the eval() function.

var r = { a:1, b: {b1:11, b2: 99}};
var s = "r.b.b2";
alert(eval(s)); // 99

I can feel people reeling in horror

5 Comments

+1 for anticipating my reeling.
The reel problem, unfortunately is that using eval prevents the compiler from making certain lexical optimisations. This means that, not only is eval itself slow, it also slows down the code around it. Oh... and pun intended.
Oh I know the pitfalls of eval(). In fact I'm off for a wire-wool shower as I feel dirty even recommending it. Andy, I've +1'd your answer as it's easily the most elegant here.
thanks this was pretty neat - but in the light of Andy's comments I can't afford a performance degrade, as the script does a lot of things.
var getObjectValue = function getter(object, key) { var value; if (typeof object === 'object' && typeof key === 'string') { value = eval('object' + '.' + key); } return value; }
20

Extending @JohnB's answer, I added a setter value as well. Check out the plunkr at

http://plnkr.co/edit/lo0thC?p=preview

enter image description here

function getSetDescendantProp(obj, desc, value) {
  var arr = desc ? desc.split(".") : [];

  while (arr.length && obj) {
    var comp = arr.shift();
    var match = new RegExp("(.+)\\[([0-9]*)\\]").exec(comp);

    // handle arrays
    if ((match !== null) && (match.length == 3)) {
      var arrayData = {
        arrName: match[1],
        arrIndex: match[2]
      };
      if (obj[arrayData.arrName] !== undefined) {
        if (typeof value !== 'undefined' && arr.length === 0) {
          obj[arrayData.arrName][arrayData.arrIndex] = value;
        }
        obj = obj[arrayData.arrName][arrayData.arrIndex];
      } else {
        obj = undefined;
      }

      continue;
    }

    // handle regular things
    if (typeof value !== 'undefined') {
      if (obj[comp] === undefined) {
        obj[comp] = {};
      }

      if (arr.length === 0) {
        obj[comp] = value;
      }
    }

    obj = obj[comp];
  }

  return obj;
}

1 Comment

I was trying to adapt Andy E's solution for get an set, and then I scrolled down and found this. Plugged this function in and watched all my tests go green. Thanks.
8

This is the simplest i could do:

var accessProperties = function(object, string){
   var explodedString = string.split('.');
   for (i = 0, l = explodedString.length; i<l; i++){
      object = object[explodedString[i]];
   }
   return object;
}
var r = { a:1, b: {b1:11, b2: 99}};

var s = "b.b2";
var o = accessProperties(r, s);
alert(o);//99

1 Comment

+1, this is very much like my solution with one significant difference. Yours will throw an error if one of the properties (except the last) doesn't exist. Mine will return undefined. Both solutions are useful in different scenarios.
4

you could also do

var s = "['b'].b2";
var num = eval('r'+s);

Comments

3

Here is an extension of Andy E's code, that recurses into arrays and returns all values:

function GetDescendantProps(target, pathString) {
    var arr = pathString.split(".");
    while(arr.length && (target = target[arr.shift()])){
        if (arr.length && target.length && target.forEach) { // handle arrays
            var remainder = arr.join('.');
            var results = [];
            for (var i = 0; i < target.length; i++){
                var x = this.GetDescendantProps(target[i], remainder);
                if (x) results = results.concat(x);
            }
            return results;
        }
    }
    return (target) ? [target] : undefined; //single result, wrap in array for consistency
}

So given this target:

var t = 
{a:
    {b: [
            {'c':'x'},
            {'not me':'y'},
            {'c':'z'}
        ]
    }
};

We get:

GetDescendantProps(t, "a.b.c") === ["x", "z"]; // true

Comments

2

I don't know a supported jQuery API function but I have this function:

    var ret = data; // Your object
    var childexpr = "b.b2"; // Your expression

    if (childexpr != '') {
        var childs = childexpr.split('.');
        var i;
        for (i = 0; i < childs.length && ret != undefined; i++) {
            ret = ret[childs[i]];
        }
    }

    return ret;

Comments

2

I've extended Andy E's answer, so that it can also handle arrays:

function getDescendantProp(obj, desc) {
    var arr = desc.split(".");

    //while (arr.length && (obj = obj[arr.shift()]));

    while (arr.length && obj) {
        var comp = arr.shift();
        var match = new RegExp("(.+)\\[([0-9]*)\\]").exec(comp);
        if ((match !== null) && (match.length == 3)) {
            var arrayData = { arrName: match[1], arrIndex: match[2] };
            if (obj[arrayData.arrName] != undefined) {
                obj = obj[arrayData.arrName][arrayData.arrIndex];
            } else {
                obj = undefined;
            }
        } else {
            obj = obj[comp]
        }
    }

    return obj;
}

There are probably more efficient ways to do the Regex, but it's compact.

You can now do stuff like:

var model = {
    "m1": {
        "Id": "22345",
        "People": [
            { "Name": "John", "Numbers": ["07263", "17236", "1223"] },
            { "Name": "Jenny", "Numbers": ["2", "3", "6"] },
            { "Name": "Bob", "Numbers": ["12", "3333", "4444"] }
         ]
    }
}

// Should give you "6"
var x = getDescendantProp(model, "m1.People[1].Numbers[2]");

1 Comment

You can just pass a regular expression to split. Just use desc..split(/[\.\[\]]+/); and the loop down the properties.
2

Performance tests for Andy E's, Jason More's, and my own solution are available at http://jsperf.com/propertyaccessor. Please feel free to run tests using your own browser to add to the data collected.

The prognosis is clear, Andy E's solution is the fastest by far!

For anyone interested, here is the code for my solution to the original question.

function propertyAccessor(object, keys, array) {
    /*
    Retrieve an object property with a dot notation string.
    @param  {Object}  object   Object to access.
    @param  {String}  keys     Property to access using 0 or more dots for notation.
    @param  {Object}  [array]  Optional array of non-dot notation strings to use instead of keys.
    @return  {*}
    */
    array = array || keys.split('.')

    if (array.length > 1) {
        // recurse by calling self
        return propertyAccessor(object[array.shift()], null, array)
    } else {
        return object[array]
    }
}

1 Comment

Nice breakdown!
0

Short answer: No, there is no native .access function like you want it. As you correctly mentioned, you would have to define your own function which splits the string and loops/checks over its parts.

Of course, what you always can do (even if its considered bad practice) is to use eval().

Like

var s = 'b.b2';

eval('r.' + s); // 99

Comments

0

Here is a a little better way then @andy's answer, where the obj (context) is optional, it falls back to window if not provided..

function getDescendantProp(desc, obj) {
    obj = obj || window;
    var arr = desc.split(".");
    while (arr.length && (obj = obj[arr.shift()]));
    return obj;
};

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.