7
\$\begingroup\$

This JS function is intended to retrieve or place a value into an object with the nested keys as a string.

For example

var obj = {a: {b: [4]}};
parse_obj_key(obj, "a.b.0") should equal 4.
parse_obj_key(obj, "a.c", 2) should add another element to "a" named "c" with value 2.

The method works, but I'd like to clean it up if possible (or a more polished implementation which is publicly available). I'd also love to know of any edge-case failures which can be found.

function parse_obj_key(obj, loc, val){
    var _o = obj;
    while (true){ 
        var pos = loc.indexOf('.');                        

        if (!_o || typeof _o != 'object'){
            $.log("Invalid obj path: " + loc + "\n" + JSON.stringify(obj));
            return null;      
        }                                                    

        if (pos === -1){
            if (val){
                _o[loc] = val;                 
                return obj;                                           
            } else {
                if (!isNaN(parseInt(loc)))
                    loc = parseInt(loc);

                return _o[loc];
            }
        }
        var part = loc.substring(0, pos);                    
        var loc = loc.substring(pos + 1);                     

        if (!isNaN(parseInt(part)))
            part = parseInt(part);   

        if (!(part in _o)){
            if (val)
                _o[part] = new object;                                        
            else
                return null;                                           
        }
        _o = _o[part];                      
    }                                                                         
}
\$\endgroup\$
1
  • \$\begingroup\$ I would suggest providing two different methods, one to get and one to set values (maybe both calling a common private method to retrieve the property). \$\endgroup\$ Jan 28, 2011 at 13:25

3 Answers 3

3
\$\begingroup\$

Here is what's wrong with your code:

  • Do not use variable names like _o. Get an editor with good auto-completion.

  • typeof _o != 'object' does not do what you think it does: typeof([1,2]) // "object". In general, doing those kinds of checks is a code smell.

  • if (!isNaN(parseInt(loc))) loc = parseInt(loc);. Confusing and not needed.
    JavaScript: ['a', 'b']["1"] // 'b'. Same goes for the other isNaN

  • in. Do not do that check. null is a value, but what you want to return is the lack of value. It is undefined in JavaScript, and it is what will be returned if there is no value.

  • Consider using split instead of indexOf and substring. It is much faster and makes the code more readable.

So, here is a neat version for you:

function chained(obj, chain, value){
    var assigning = (value !== undefined);
    // split chain on array and property accessors
    chain = chain.split(/[.\[\]]+/);       
    // remove trailing ']' from split   
    if (!chain[chain.length - 1]) chain.pop();
    // traverse 1 level less when assigning
    var n = chain.length - assigning; 
    for (var i = 0, data = obj; i < n; i++) {
        data = data[chain[i]];
        // if (data === undefined) return; // uncomment to handle bad chain keys      
    }

    if (assigning) {
        data[chain[n]] = value;
        return obj;
    } else {
        return data;       
    }
}

Blogged: http://glebm.blogspot.com/2011/01/javascript-chained-nested-assignment.html

Please come up with further improvements :)

\$\endgroup\$
8
  • \$\begingroup\$ Could you elaborate on how your solution is an improvement with regards to OP's code? \$\endgroup\$ Jan 28, 2011 at 13:19
  • \$\begingroup\$ @Eric Done. Feel free to add more. \$\endgroup\$
    – glebm
    Jan 29, 2011 at 9:45
  • 2
    \$\begingroup\$ I mostly agree with your comments, except "Do not use strict comparison". I would rather suggest to use strict comparison all the time, trying to focus on "The Good Parts" (c) of JavaScript rather than its quirks. +1 \$\endgroup\$ Jan 29, 2011 at 17:09
  • \$\begingroup\$ I used _o to signify that it's a temporary var just to be mashed about in the loop, e.g. that it only meaningful for it's ref to the real obj. I intended the typeof to work the way it did, I want to know if it's an array or an object (e.g. not a primitive), will it not work for this purpose? \$\endgroup\$
    – Zack Bloom
    Jan 29, 2011 at 23:19
  • \$\begingroup\$ I was not completely correct about your use of typeof. It will work for checking whether something is a primitive or not. However, if you try parse_obj_key("awesome_string", "length") it won't work because "object" is not the same as something that responds to methods. Cheers. \$\endgroup\$
    – glebm
    Jan 30, 2011 at 5:36
0
\$\begingroup\$

I know little about javascript, and thought yours solution is fine. glebm's code seems not working for parse_obj_key(obj, "a.c.c.c", 2)

I trying to modify your code to a recursion style, and it works, the "new object" goes wrong on my firefox, so I change it to = {}

function parse_obj_key(obj, loc, val) {
    if(!obj || typeof obj != 'object') {
        alert("error")
        return null
    }
    var pos = loc.indexOf('.');
    var part = loc.substring(0, pos);
    var endpart = loc.substring(pos+1);

    if (!isNaN(parseInt(part)))
        part = parseInt(part);

    if (pos === -1) {
        if (val) {
            obj[loc] = val
            return obj
        }
        else
            return obj[loc]
    }

    if (val) {
        if (!(part in obj)) 
            obj[part] = {}

        parse_obj_key(obj[part], endpart, val)
        return obj
    }
    else {
        return parse_obj_key(obj[part], endpart, val)
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ I much prefer a recursive style like this to "while (true)" if you can get away with it. I would say, though, that you might want to consider refactoring the validation of obj into a separate method. parse_obj_key would validate the arguments, then call into the recursive helper function. That way the arguments are re-validated on every recursion. Not a big problem in this instance, but a pattern to consider for deeper recursion or move complex argument validation. \$\endgroup\$
    – ICR
    Jan 29, 2011 at 9:10
0
\$\begingroup\$

My code is based off glebm's, but I've changed it so that array lookups must always use the . syntax. I made a few other tweaks such as creating an object chain if assigning to a nested key that doesn't yet exist.

function parse_obj_key(obj, key, value){
    var is_assigning = (value !== undefined);
    key = key.split('.');     

    var data = obj;
    var length = key.length - is_assigning;

    for (var i = 0; i < length; i++) {
        var child = data[key[i]];
        var has_child = (child !== undefined);

        if (!has_child && !is_assigning) {
            $.log("Invalid obj path: " + key + "\n" + JSON.stringify(obj));
            return null;
        }

        if (!has_child) {
            child = {};
        data[key[i]] = child;
        }

        data = child;
    }

    if (is_assigning) {
        data[key[i]] = value;
    } else {
        return data;       
    }
}

If you're trying to create an optional parameter, you should check if it is undefined using if (param !== undefined), rather than just if (param), otherwise you'll have trouble setting falsy values such as false, null, 0 and "".

One of the things that allows this code to be much shorter is the observation that in javascript, arr['a']['b'][0] is the same as arr['a']['b']['0'] - that is to say, array lookup using an integer is no different from array lookup using a string that contains a number. This avoids the need to parse the numbers like you do.

The other major saver is the use of split rather than indexing, which makes it easy to for loop over the parts of the key.

\$\endgroup\$
2
  • \$\begingroup\$ Your code fails to return null on parse_obj_key({a: null}, 'a'). Replacing (!child) with child !== undefined might fix it. Cheers \$\endgroup\$
    – glebm
    Jan 29, 2011 at 9:47
  • \$\begingroup\$ Cheers. I should listen to my own advice :P \$\endgroup\$
    – ICR
    Jan 30, 2011 at 10:17

Your Answer

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

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