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.

Geri Borbás
  • 15,810
  • 18
  • 109
  • 172
  • 3
    what is wrong with `data[memberName][propertyName]` ??? – mohkhan Jul 16 '13 at 17:51
  • or data.memberName.propertyName? – Stefan Jul 16 '13 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... – dandavis Jul 16 '13 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? – Geri Borbás Jul 17 '13 at 09:38
  • @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. – Geri Borbás Jul 17 '13 at 10:19

8 Answers8

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 !

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
jeeeyul
  • 3,727
  • 1
  • 24
  • 37
4

With lodash there is a simple method for doing this.

_.get()


enter image description here

jose920405
  • 7,982
  • 6
  • 45
  • 71
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.

Community
  • 1
  • 1
Geri Borbás
  • 15,810
  • 18
  • 109
  • 172
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."
dandavis
  • 16,370
  • 5
  • 40
  • 36
  • Hey, thanks for the non-eval suggestion, also the defineProperty. Now I got the ultimate solution, gonna post soon. – Geri Borbás Feb 08 '14 at 22:47
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)
goodson
  • 727
  • 2
  • 14
  • 24
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]);
Louis Ricci
  • 20,804
  • 5
  • 48
  • 62
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

Flavien Volken
  • 19,196
  • 12
  • 100
  • 133