131

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?

Andy E
  • 338,112
  • 86
  • 474
  • 445
msanjay
  • 2,255
  • 2
  • 19
  • 19
  • 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. – Blazemonger Nov 08 '11 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. – msanjay Nov 08 '11 at 17:50

13 Answers13

143

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
Andy E
  • 338,112
  • 86
  • 474
  • 445
  • 1
    jshint told me to rewrite: while(arr.length) { obj = obj[arr.shift()]; } –  Mar 07 '15 at 22:51
  • 2
    @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. – Andy E Mar 08 '15 at 19:46
  • you can do `while(arr.length && obj) { obj = obj[arr.shift()]; }` – Peter Ajtai Dec 24 '15 at 21:39
  • a[1] doesn't work :( – jdnichollsc Aug 05 '16 at 22:20
  • @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. – Andy E Aug 08 '16 at 08:16
  • @AndyE what do you think about the following solution? https://gist.github.com/jdnichollsc/a60896beda77073c2e2b8358baf3eb7c – jdnichollsc Aug 08 '16 at 14:49
137

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);
Liam
  • 27,717
  • 28
  • 128
  • 190
AmmarCSE
  • 30,079
  • 5
  • 45
  • 53
  • 5
    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 ;` – Björn Apr 01 '16 at 06:18
  • @AmmarCSE would this work if one of the nested properties is array? For example `var key = "b.b2[0].c"` ? – Primoz Rome Apr 29 '16 at 07:27
  • @AmmarCSE actually it does not work. Would love a tip how could I update your example to support that. Thanks – Primoz Rome Apr 29 '16 at 07:42
  • 1
    @PrimozRome, hey there! It can work if you can use `b.b2.0.c` instead. See https://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! :-) – AmmarCSE Apr 29 '16 at 10:39
39

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
Matheus Dal'Pizzol
  • 5,735
  • 3
  • 19
  • 29
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

Rory McCrossan
  • 331,213
  • 40
  • 305
  • 339
  • 26
    +1 for anticipating my reeling. – jrummell Nov 08 '11 at 14:37
  • 6
    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. – Andy E Nov 08 '11 at 14:42
  • 3
    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. – Rory McCrossan Nov 08 '11 at 14:45
  • 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. – msanjay Nov 08 '11 at 17:55
  • var getObjectValue = function getter(object, key) { var value; if (typeof object === 'object' && typeof key === 'string') { value = eval('object' + '.' + key); } return value; } – Deepak Acharya Feb 24 '16 at 15:31
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;
}
Thomas
  • 446
  • 4
  • 10
Jason More
  • 6,983
  • 6
  • 43
  • 52
  • 2
    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. – nbering Aug 25 '15 at 00:14
  • Thank you for adding set functionality. – risyasin Nov 19 '15 at 07:56
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
Nicola Peluchetti
  • 76,206
  • 31
  • 145
  • 192
  • 2
    +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. – Andy E Nov 08 '11 at 14:57
4

you could also do

var s = "['b'].b2";
var num = eval('r'+s);
Manuel van Rijn
  • 10,170
  • 1
  • 29
  • 52
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
Iain Ballard
  • 4,433
  • 34
  • 39
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]");
JohnB
  • 451
  • 5
  • 7
  • You can just pass a regular expression to `split`. Just use `desc..split(/[\.\[\]]+/);` and the loop down the properties. – Matt Clarkson May 21 '14 at 14:44
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]
    }
}
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;
Ferran Basora
  • 3,097
  • 3
  • 19
  • 13
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;
};
Community
  • 1
  • 1
adardesign
  • 33,973
  • 15
  • 62
  • 84
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
jAndy
  • 231,737
  • 57
  • 305
  • 359