如何在Javascript中通过其keyPath访问属性?

8

I have

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

我真的希望像这样访问它:

number = data['first.number'];

实际上,可以更加灵活地实现,例如:

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

你有什么轻量级的库或代码片段可以建议吗?这个 - https://github.com/martinvl/KVCObject - 很酷,但对于这个项目来说有点过重了。


3
data[memberName][propertyName]有什么问题? - mohkhan
或者 data.memberName.propertyName? - Stefan
1
如果您的路径没有非单词字符,您可以评估该路径。您还可以使用[].map()或循环在爆炸的路径上,每次向更深处迈出一步,将分支设置为根并继续... - dandavis
@dandavis,其中一部分将是我的解决方案,谢谢。Eval听起来是一个非常简单的解决方案。你能发布一个答案让我接受吗? - Geri Borbás
@mohkhan:我将控制器对象的引用(数组keyPaths)存储在DOM元素数据集上。例如,data-key-path="viewController.controls.transform.opacitySlider";然后,通过获取文档的keyPath值,就可以简单地引用该对象。 - Geri Borbás
8个回答

23
你可以使用reduce函数轻松解决keypath问题,而不使用任何库。
首先,我们创建一个名为target的示例对象,并在其中嵌套一些对象:
const target = {
    foo: {
        bar: {
            example: 65
        }
    }
};

然后,我们定义一个变量keypath,其中包含键路径字符串: 我们想要访问目标对象内的example属性。

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

今天开始要付出努力了!将Keypath使用点分隔符拆分并获得一个键数组。我们使用reduce函数迭代这个数组,并在每次迭代时返回一个新对象。

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

最终,结果变量的值为65。它起作用了!


1
没问题,我已经包含了一个解释。 - Thibault Boursier
4
优雅的解决方案! - cutsoy
你需要为每个条目创建一个键路径。 - FabricioG
你能解释一下你的意思吗,@FabricioG? - Thibault Boursier

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

我喜欢这个库,但是语法开销有点大。我更喜欢使用vanilla - Geri Borbás

4

使用 lodash 很容易实现此操作。

_.get()


enter image description here


3
基于 @dandavis相当简单的建议,我可以将访问器设置为原型属性。 没有 eval,同时使用 Object.defineProperty 时不会影响到枚举 使用 Object.prototype
实际上,解决方案如下:
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);
}});

测试正常:

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);

使用hu作为语言时,每个数据成员的输出都是相同的。


1
如果我是你,我不会修改Object.prototype。如果我这样做了,我会使用Object.defineProperty()来覆盖它,而不是像上面的代码一样留下for循环中断的原型属性赋值... - dandavis
好的,谢谢建议。我会尽快修改答案。 - Geri Borbás
修改完成,现在可以安全投入生产。 - Geri Borbás

3

如果你所有的路径都是基于点的(没有使用数组语法),你可以使用eval或一个简单的滑动递归函数:

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."

嘿,感谢您提供的非评估建议和defineProperty。现在我有了终极解决方案,很快就会发布。 - Geri Borbás

1
我有点晚了,但我需要同样的东西,并且认为这很简单实用。(它期望你split('.')your.key.path变成['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)


0

ES2015 可以使用解构:

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

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

更多例子


0
创建一个辅助函数,该函数可以读取可变数量的参数或参数数组。
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));

编辑 您还可以创建一个帮助函数来反规范化您的对象,但只有在反规范化后读取值,因为编辑值会导致冲突,因为您的对象将具有内部对象值的副本。

示例:

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

反规范化代码

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]);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接