Access property via it's keyPath in Javascript?
Asked Answered
N

8

8

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.

Nopar answered 16/7, 2013 at 17:50 Comment(5)
what is wrong with data[memberName][propertyName] ???Snappish
or data.memberName.propertyName?Bethanybethe
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...Kado
@Kado Some of that will be my solution, thanks. Eval sounds extremly simple solution. Can you post as an answer to accept?Hallmark
@mohkhan: I store references (array keyPaths) to controller objects on DOM elements dataset. Like data-key-path="viewController.controls.transform.opacitySlider"; Then have a reference to that object simply by get the value for the keyPath of document.Hallmark
N
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.

Nopar answered 17/7, 2013 at 9:52 Comment(3)
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...Kado
Exactly, thanks for the suggestion. Will modify the answer as well soon.Hallmark
Modifided, now it is production safe.Hallmark
P
23

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 !

Parcenary answered 19/6, 2017 at 9:48 Comment(4)
No problem, I included an explanation.Parcenary
Elegant solution!Rawls
You'd have to make a keypath for every entryStibnite
Could you explain what do you mean, @Stibnite ?Parcenary
B
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
Bluegreen answered 8/2, 2014 at 10:29 Comment(1)
I do like, nice lib. The syntax overhead is a bit too much for me. I prefer vanilla.Hallmark
E
4

With lodash there is a simple method for doing this.

_.get()


enter image description here

Elbertine answered 24/9, 2020 at 20:35 Comment(0)
N
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.

Nopar answered 17/7, 2013 at 9:52 Comment(3)
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...Kado
Exactly, thanks for the suggestion. Will modify the answer as well soon.Hallmark
Modifided, now it is production safe.Hallmark
K
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."
Kado answered 17/7, 2013 at 15:44 Comment(1)
Hey, thanks for the non-eval suggestion, also the defineProperty. Now I got the ultimate solution, gonna post soon.Hallmark
U
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)
Unfair answered 8/3, 2019 at 22:59 Comment(0)
E
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]);
Erector answered 16/7, 2013 at 18:7 Comment(0)
T
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

Thusly answered 23/1, 2018 at 8:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.