9

I have

data = 
{
    'first': {
        'number': 1,
        'text': 'Ya.'
    },
    'second': {
        'number': 10,
        'text': 'Da.'
    }
};

And I really want to access it like:

number = data['first.number'];

Actually in a more flexible way, like:

numberOrText = data[memberName+'.'+propertyName];

Is there any lightweight library, or snippet you can suggest? This is - https://github.com/martinvl/KVCObject - so cool, but a bit overhead for this.

6
  • 3
    what is wrong with data[memberName][propertyName] ??? Commented Jul 16, 2013 at 17:51
  • or data.memberName.propertyName? Commented Jul 16, 2013 at 17:51
  • 1
    if your path doesn't have non-wordy chars, you can eval the path. you can also use [].map() or a loop upon an exploded path to step a step deeper each time, setting branch to root and continuing... Commented Jul 16, 2013 at 17:53
  • @dandavis Some of that will be my solution, thanks. Eval sounds extremly simple solution. Can you post as an answer to accept? Commented Jul 17, 2013 at 9:38
  • 1
    path-value is a lightweight library which can do all that. Commented Apr 5 at 22:12

8 Answers 8

24

You can easily resolve keypath with reduce function, without using any library.

First, we are creating an example object, called target, with some nested objects inside :

const target = {
    foo: {
        bar: {
            example: 65
        }
    }
};

Then, we define a variable keypath containing keypath string : we want to access example property inside our target object.

const keypath = 'foo.bar.example';    ​

The hard work begins today ! Keypath is splitted by dot separator and we obtain a keys array. We iterate over this array (with reduce function) and for each iteration, we return a new object.

const result = keypath.split('.').reduce((previous, current) => previous[current], target);

Finally, result variable value is 65. It works !

Sign up to request clarification or add additional context in comments.

4 Comments

No problem, I included an explanation.
Elegant solution!
You'd have to make a keypath for every entry
Could you explain what do you mean, @FabricioG ?
4

I think You may like underscore-keypath.

var foo = {
  bar : {
    name : "Cool!"
  },
  scores : [55, 27, 100, 33]
};

_(foo).valueForKeyPath("bar.name");           // --> "Cool!"
_(foo).setValueForKeyPath("bar.name", "BAR"); // --> sets foo.bar.name as "BAR"
_(foo).valueForKeyPath("scores.@max");        // --> 100

1 Comment

I do like, nice lib. The syntax overhead is a bit too much for me. I prefer vanilla.
4

With lodash there is a simple method for doing this.

_.get()


enter image description here

Comments

3

if you have all dot-based paths (no array syntax), you can use eval or a simple sliding recursive function:

var data = {
    'first': {
        'number': 1,
        'text': 'Ya.'
    },
    'second': {
        'number': 10,
        'text': 'Da.'
    }
};


// the simple but discouraged way using eval:
alert(
  eval( 
     "data.second.text"
  )
); //shows "Da."


// a non-eval looping solution take s bit more code, but can be faster to execute:

function resolve(obj, path){
  var r=path.split(".");
  if(path){return resolve(obj[r.shift()], r.join("."));}
 return obj
}

alert(
   resolve(data, "first.text")
); //shows: "Ya."

1 Comment

Hey, thanks for the non-eval suggestion, also the defineProperty. Now I got the ultimate solution, gonna post soon.
3

Based on @dandavis pretty simple suggestions, I can set up accessors as prototype properties.

No eval, also leave Object.prototype untouched in terms of enumerating using Object.defineProperty.

The solution actually goes like this:

function stringContains(string, value)
{ return string.indexOf(value) != -1; }

Object.defineProperty(Object.prototype, "setValueForKey", { value: function(value, key)
{ this[key] = value; }});

Object.defineProperty(Object.prototype, "setValueForKeyPath", { value: function(value, keyPath)
{
    if (keyPath == null) return;
    if (stringContains(keyPath, '.') == false) { this.setValueForKey(value, keyPath); return; }

    var chain = keyPath.split('.');
    var firstKey = chain.shift();
    var shiftedKeyPath = chain.join('.');

    this[firstKey].setValueForKeyPath(value, shiftedKeyPath);
}});

Object.defineProperty(Object.prototype, "getValueForKey", { value: function(key)
{ return this[key]; }});

Object.defineProperty(Object.prototype, "getValueForKeyPath", { value: function(keyPath)
{
    if (keyPath == null) return;
    if (stringContains(keyPath, '.') == false) { return this.getValueForKey(keyPath); }

    var chain = keyPath.split('.');
    var firstKey = chain.shift();
    var shiftedKeyPath = chain.join('.');

    return this[firstKey].getValueForKeyPath(shiftedKeyPath);
}});

Test are fine:

data = {
    'name' : 'data',
    'first': {
        'number': 1,
        'text': 'Ya.',
        'meta' : {
            'lang' : 'en'
        }
    },
    'second': {
        'number': 10,
        'text': 'Ba.',
        'meta' : {
            'lang' : 'en'
        }
    },
    'third': {
        'number': 100,
        'text': 'Da.',
        'meta' : {
            'lang' : 'hu'
        }
    }
};

data.setValueForKey('chunk', 'name');
data.setValueForKeyPath('blob', 'name');

var thirdLanguage = data.getValueForKeyPath('third.meta.lang');
data.setValueForKeyPath(thirdLanguage, 'first.meta.lang');
data.setValueForKeyPath(thirdLanguage, 'second.meta.lang');

log(data);

Output is the same with hu as language in every data member.

3 Comments

i would NOT modify Object.prototype if i were you. If i did, i would cover it up using Object.defineProperty() instead of leaving the for-loop-breaking prototype property assignment as it is in the code above...
Exactly, thanks for the suggestion. Will modify the answer as well soon.
Modifided, now it is production safe.
1

I'm a little late to this, but I needed the same thing, and figured this was small and functional. (It expects you split('.') your.key.path to become ['your', 'key', 'path']

data = 
{
    'first': {
        'number': 1,
        'text': 'Ya.'
    },
    'second': {
        'number': 10,
        'text': 'Da.',
        'array': ['a', {'b':'bar'}, 'c']
    }
};

function valueAtPath(object, path) {
    if (!object || path.length === 0) return object
    return valueAtPath(object[path.shift()], path)
}

function setValueAtPath(object, path, value) {
    if (!object || path.length === 0) return null
    if (path.length === 1) object[path[0]] = value
    else return setValueAtPath(object[path.shift()], path, value)
}

console.log(valueAtPath(data, ['second', 'array', 1, 'b']))

setValueAtPath(data, ['second', 'array', 1, 'b'], 'foo')
console.log(data)

Comments

0

Make a helper function that reads a variable number of arguments or an array of parameters.

Object.prototype.$ = function() {
    var result = this;
    var list;
    /*
    Array .$(["first", "text"])
    String .$("second.number")
    String Parameters .$("first", "text")
    */
    if(arguments.length == 1 && Object.prototype.toString.call(arguments[0]) === "[object Array]")
        list = arguments[0];
    else if(arguments.length == 1 && typeof(arguments[0]) == 'string' && arguments[0].indexOf(".") >= 0)
        list = arguments[0].split(".");
    else
        list = arguments;
    for(var i=0; i<list.length; i++)
        result = result[list[i]];
    return result;
}

// test it
data = 
{
    'first': {
        'number': 1,
        'text': 'Ya.'
    },
    'second': {
        'number': 10,
        'text': 'Da.'
    }
};
var s = "second";
var s2 = "first.number";
console.log(data.$("first", "text"));
console.log(data.$(s, "number"));
console.log(data.$(["first", "number"]));
console.log(data.$(s2));

edit You could also make a helper function to DEnormalize your object, but only read values after you denormalize it because editing values will cause conflicts since your object will have copies of inner object values.

Example:

data["first"]["number"] == data["first.number"];
data["first.number"] = -1;
data["first"]["number"] != data["first.number"];

De-normalize code

function denormalize(obj, lv) {
    var more = false;
    for(var k in obj) {
        if(k.split(".").length == lv) {
            var node = obj[k]
            if(node && typeof(node) == 'object') {
                more = true;
                for(var k2 in node) {
                    obj[k + "." + k2] = node[k2];
                }
            }
        }
    }
    if(more)
        denormalize(obj, lv + 1);
    return obj;
}

// test it
data = 
{
    'first': {
        'number': 1,
        'text': 'Ya.'
    },
    'second': {
        'number': 10,
        'text': 'Da.'
    },
    "third": [{"number": 5, "text": "meh"},{"number": 6, "text": "beh"}]
};
denormalize(data, 1);
for(var k in data)
    console.log(k + " : " + data[k]);

Comments

0

ES2015 can use the destructuring:

data = 
{
    'first': {
        'number': 1,
        'text': 'Ya.'
    },
    'second': {
        'number': 10,
        'text': 'Da.'
    }
};

const {first:{number: yourValue}} = data;
console.log(yourValue); // 1

More examples

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.