通过字符串路径访问嵌套的JavaScript对象和数组

675

我有一个类似这样的数据结构:

var someObject = {
    'part1' : {
        'name': 'Part 1',
        'size': '20',
        'qty' : '50'
    },
    'part2' : {
        'name': 'Part 2',
        'size': '15',
        'qty' : '60'
    },
    'part3' : [
        {
            'name': 'Part 3A',
            'size': '10',
            'qty' : '20'
        }, {
            'name': 'Part 3B',
            'size': '5',
            'qty' : '20'
        }, {
            'name': 'Part 3C',
            'size': '7.5',
            'qty' : '20'
        }
    ]
};

我希望可以使用这些变量来访问数据:

var part1name = "part1.name";
var part2quantity = "part2.qty";
var part3name1 = "part3[0].name";

part1name应该填写someObject.part1.name的值,即"Part 1"。part2quantity同理,应填写60。

是否有办法使用纯JavaScript或JQuery实现这一点?


不确定你在这里问什么?你想查询part1.name并返回文本"part1.name"吗?还是你想要一种方法来获取存储在part1.name中的值? - BonyT
你尝试过像这样做吗:var part1name = someObject.part1name; - Rafay
1
@BonyT:我想查询someObject.part1.name并返回它的值(“Part 1”)。但是,我希望查询(我称之为“键”)存储在变量'part1name'中。感谢您的回复。@3nigma:我当然可以这样做。但这不是我的意图。感谢回复。 - Komaruloh
1
在重复的答案中,我喜欢fyr的回答。https://dev59.com/Gmoy5IYBdhLWcg3wCpoz - Steve Black
显示剩余2条评论
46个回答

676

我基于一些类似的代码编写了这个,看起来它能工作:

Object.byString = function(o, s) {
    s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
    s = s.replace(/^\./, '');           // strip a leading dot
    var a = s.split('.');
    for (var i = 0, n = a.length; i < n; ++i) {
        var k = a[i];
        if (k in o) {
            o = o[k];
        } else {
            return;
        }
    }
    return o;
}

使用方法:

Object.byString(someObj, 'part3[0].name');

可以在http://jsfiddle.net/alnitak/hEsys/查看工作示例。

编辑:有些人注意到,如果向该函数传递一个字符串,其中最左边的索引与对象中正确嵌套的条目不对应,则会抛出错误。 这是一个有效的关注点,但我认为最好在调用时使用try / catch块来处理,而不是让此函数在无效索引处默默返回undefined


41
这个非常好用。请将其封装为一个 Node 包并分享到互联网上。 - t3dodson
18
@t3dodson 我刚刚做了这个:https://github.com/capaj/object-resolve-path 但请注意,当您的属性名称本身包含“[]”时,它可能无法正常工作。正则表达式将其替换为“。”并且不会按预期工作。 - Capaj
52
很棒的东西;使用 Lodash 库,我们还可以这样做:_.get(object, nestedPropertyString) - ian
21
这条评论可能会在众多评论中被忽略,但是如果您试图访问不存在的属性,它就会出错。例如:'part3[0].name.iDontExist'。在 if in 中添加一个检查以判断 o 是否为对象可以解决这个问题(具体如何实现取决于你)。请参见更新后的 fiddle:http://jsfiddle.net/hEsys/418/。 - ste2425
3
@ThatGuyRob 引入第三方库并不总是“更好”,而且在我写这篇答案时,那种方法甚至还不存在。 - Alnitak
显示剩余27条评论

299

现在可以使用lodash的_.get(obj, property)实现此功能。请参见https://lodash.com/docs#get

以下是文档中提供的示例:

var object = { 'a': [{ 'b': { 'c': 3 } }] };

_.get(object, 'a[0].b.c');
// → 3

_.get(object, ['a', '0', 'b', 'c']);
// → 3

_.get(object, 'a.b.c', 'default');
// → 'default'

12
这应该是唯一被接受的答案,因为这是唯一适用于点和括号语法的答案,并且在路径中的键的字符串中有“[]”时不会失败。 - Capaj
12
这个。此外,它支持 _.set(...) - Josh C.
如果对象未找到会发生什么? - DDave
@DDave 如果传递给对象的值未定义或不是对象,则 _.get 将显示与在提供的对象中未找到键时相同的行为。例如 _.get(null, "foo") -> undefined_.get(null, "foo", "bar") -> "bar"。但是,此行为未在文档中定义,因此可能会更改。 - Ian Walker-Sperber
22
你是在开玩笑吗,@Capaj?谁不想或不能使用lodash? - Andre Figueiredo
5
@ Capaj...我不想为几行代码使用外部库。 - run_the_race

266

这是我使用的解决方案:

function resolve(path, obj=self, separator='.') {
    var properties = Array.isArray(path) ? path : path.split(separator)
    return properties.reduce((prev, curr) => prev?.[curr], obj)
}

使用示例:

// accessing property path on global scope
resolve("document.body.style.width")
// or
resolve("style.width", document.body)

// accessing array indexes
// (someObject has been defined in the question)
resolve("part3.0.size", someObject) // returns '10'

// accessing non-existent properties
// returns undefined when intermediate properties are not defined:
resolve('properties.that.do.not.exist', {hello:'world'})

// accessing properties with unusual keys by changing the separator
var obj = { object: { 'a.property.name.with.periods': 42 } }
resolve('object->a.property.name.with.periods', obj, '->') // returns 42

// accessing properties with unusual keys by passing a property name array
resolve(['object', 'a.property.name.with.periods'], obj) // returns 42

限制:

  • 不能使用方括号 ([]) 来表示数组的索引——不过可以像上面所示一样,在分隔符令牌(如点号 .)之间指定数组索引。

7
使用 reduce 是一个很好的解决方案(也可以使用 Underscore 或 Lodash 库中的 _.reduce())。 - Alp
5
我认为 self 在这里可能未定义。你是不是指的是 this - Platinum Azure
3
这是我对按路径设置值的补充:http://pastebin.com/jDp5sKT9 - mroach
1
@SC1000 很好的想法。这个答案是在大多数浏览器中都没有默认参数之前编写的。我会将其更新为“function resolve(path, obj=self)”,因为将全局对象作为默认值引用是有意义的。 - speigg
5
@AdamPlocher 我知道这已经有点过时了,但这是我翻译成TypeScript后的代码:export function resolvePath(path: string | string[], obj: any, separator = '.') { const properties = Array.isArray(path) ? path : path.split(separator); return properties.reduce((prev, curr) => prev && prev[curr], obj); } - crush
显示剩余8条评论

240

ES6:Vanila JS 中只需一行代码(如果未找到,它将返回 null 而不是错误):


'path.string'.split('.').reduce((p,c)=>p&&p[c]||null, MyOBJ)

例如:

'a.b.c'.split('.').reduce((p,c)=>p&&p[c]||null, {a:{b:{c:1}}})

使用可选链操作符:

'a.b.c'.split('.').reduce((p,c)=>p?.[c], {a:{b:{c:1}}})

为了一个可直接使用的函数,它也能识别 false、0 和负数,并接受默认值作为参数:

const resolvePath = (object, path, defaultValue) => path
   .split('.')
   .reduce((o, p) => o ? o[p] : defaultValue, object)

使用示例:

resolvePath(window,'document.body') => <body>
resolvePath(window,'document.body.xyz') => undefined
resolvePath(window,'document.body.xyz', null) => null
resolvePath(window,'document.body.xyz', 1) => 1

奖金:

设置路径(由@rob-gordon请求),您可以使用:

const setPath = (object, path, value) => path
   .split('.')
   .reduce((o,p,i) => o[p] = path.split('.').length === ++i ? value : o[p] || {}, object)

例子:

let myVar = {}
setPath(myVar, 'a.b.c', 42) => 42
console.log(myVar) => {a: {b: {c: 42}}}

使用 [] 访问数组:

const resolvePath = (object, path, defaultValue) => path
   .split(/[\.\[\]\'\"]/)
   .filter(p => p)
   .reduce((o, p) => o ? o[p] : defaultValue, object)

示例:

const myVar = {a:{b:[{c:1}]}}
resolvePath(myVar,'a.b[0].c') => 1
resolvePath(myVar,'a["b"][\'0\'].c') => 1

3
我喜欢这个技巧。虽然它看起来很凌乱,但我想在作业中使用这个技巧。以下是需要翻译的代码:let o = {a:{b:{c:1}}}; let str = 'a.b.c'; str.split('.').splice(0, str.split('.').length - 1).reduce((p,c)=>p&&p[c]||null, o)[str.split('.').slice(-1)] = "some new value"; - rob-gordon
5
我喜欢使用reduce的想法,但你对于0undefinednull值的逻辑似乎有些问题。例如{a:{b:{c:0}}}返回了null而非0。也许显式地检查nullundefined可以解决这些问题。(p,c)=>p === undefined || p === null ? undefined : p[c] - SmujMaiku
1
嗨@SmujMaiku,"ready to use"函数对于'0'、'undefined'和'null'的返回值是正确的,我刚在控制台上进行了测试:resolvePath({a:{b:{c:0}}},'a.b.c',null) => 0; 它检查键是否存在,而不是值本身,这避免了多次检查。 - Adriano Spadoni
1
@AdrianoSpadoni 对不起,我觉得我指出问题的方式不太礼貌。祝你有美好的一天。 - ADJenks
1
请注意,带有 defaultValue 的版本在某些情况下仍会返回 undefined ——例如 resolvePath({profile: {name: 'Bob'}}, 'profile.email', 'not set')。为了解决这个问题,最后一行应该是 .reduce((o, p) => o?.[p] ?? defaultValue, object) - dsl101
显示剩余7条评论

67

你需要自己解析这个字符串:

function getProperty(obj, prop) {
    var parts = prop.split('.');

    if (Array.isArray(parts)) {
        var last = parts.pop(),
        l = parts.length,
        i = 1,
        current = parts[0];

        while((obj = obj[current]) && i < l) {
            current = parts[i];
            i++;
        }

        if(obj) {
            return obj[last];
        }
    } else {
        throw 'parts is not valid array';
    }
}

这要求您还需使用点符号表示数组下标:

var part3name1 = "part3.0.name";

这使得解析更加容易。

演示


@Felix Kling:您的解决方案确实给了我所需的东西。非常感谢您。但是Alnitak也提供了不同的方法,似乎也可以工作。由于我只能选择一个答案,我将选择Alnitak提供的答案。并不是说他的解决方案比你好之类的。无论如何,我非常感激您的解决方案和付出。 - Komaruloh
@Felix FWIW - 从[]语法转换为属性语法非常简单。 - Alnitak
5
如果你将while循环改为while (l > 0 && (obj = obj[current]) && i < l),那么这段代码也适用于没有点的字符串。 - Snea
1
说实话,这是更好的答案,因为你可以实际改变obj[last]的值,但如果你用另一种方式做就无法改变该值。 - CuddleBunny
这比其他的更灵活 :D - Kavinda Jayakody
显示剩余3条评论

43

适用于数组/对象内的数组,对无效值进行防御。

/**
 * Retrieve nested item from object/array
 * @param {Object|Array} obj
 * @param {String} path dot separated
 * @param {*} def default value ( if result undefined )
 * @returns {*}
 */
function path(obj, path, def){
    var i, len;

    for(i = 0,path = path.split('.'), len = path.length; i < len; i++){
        if(!obj || typeof obj !== 'object') return def;
        obj = obj[path[i]];
    }

    if(obj === undefined) return def;
    return obj;
}

//////////////////////////
//         TEST         //
//////////////////////////

var arr = [true, {'sp ace': true}, true]

var obj = {
  'sp ace': true,
  arr: arr,
  nested: {'dotted.str.ing': true},
  arr3: arr
}

shouldThrow(`path(obj, "arr.0")`);
shouldBeDefined(`path(obj, "arr[0]")`);
shouldBeEqualToNumber(`path(obj, "arr.length")`, 3);
shouldBeTrue(`path(obj, "sp ace")`);
shouldBeEqualToString(`path(obj, "none.existed.prop", "fallback")`, "fallback");
shouldBeTrue(`path(obj, "nested['dotted.str.ing'])`);
<script src="https://cdn.rawgit.com/coderek/e7b30bac7634a50ad8fd/raw/174b6634c8f57aa8aac0716c5b7b2a7098e03584/js-test.js"></script>


11
谢谢,这是最好的和性能最佳的答案 - http://jsfiddle.net/Jw8XB/1/ - Dominic
@Endless,我想强调路径应该用点分隔项。花括号不起作用。即要访问数组中的第一个项目,请使用“0.sp ace”。 - TheZver

33

这个东西可能永远看不到天日...但是无论如何,这里它是。

  1. .替换[]括号语法
  2. .字符分割字符串
  3. 移除空字符串
  4. 查找路径(否则为undefined

(如果要查找对象的路径,请使用此pathTo 解决方案。)

// "one liner" (ES6)

const deep_value = (obj, path) => 
path
    .replace(/\[|\]\.?/g, '.')
    .split('.')
    .filter(s => s)
    .reduce((acc, val) => acc && acc[val], obj);
    
// ... and that's it.

var someObject = {
    'part1' : {
        'name': 'Part 1',
        'size': '20',
        'qty' : '50'
    },
    'part2' : {
        'name': 'Part 2',
        'size': '15',
        'qty' : '60'
    },
    'part3' : [
        {
            'name': 'Part 3A',
            'size': '10',
            'qty' : '20'
        }
        // ...
    ],
    'pa[rt3' : [
        {
            'name': 'Part 3A',
            'size': '10',
            'qty' : '20'
        }
        // ...
    ]
};

console.log(deep_value(someObject, "part1.name"));               // Part 1
console.log(deep_value(someObject, "part2.qty"));                // 60
console.log(deep_value(someObject, "part3[0].name"));            // Part 3A
console.log(deep_value(someObject, "part3[0].....name"));        // Part 3A - invalid blank paths removed
console.log(deep_value(someObject, "pa[rt3[0].name"));           // undefined - name does not support square brackets


请注意,这将会默默地吞噬许多无效的路径,例如 ...one...two...。如果要进行智能处理,您不能仅仅用.替换[],您必须删除每个],然后用.替换每个不在开头的[,最后删除所有剩余的[ - vitaly-t
1
是的,今天有很多库可以进行冗长语法解析。不幸的是,这样做会付出代价,比起 a.0.b.1 的简单索引方法来说,速度慢了很多,而后者对于 JavaScript 来说是自然的,并且更快。一个简单的分割就足够了。 - vitaly-t
完全正确。这是一个权衡。如果您可以控制输入(例如,控制使用方括号),那就更好了 - 您将节省加载臃肿库的时间。 - Nick Grealy
我确实尝试过,在我的path-value中,虽然处理方括号的代码并没有变得臃肿,但它总是对性能产生了巨大的影响。因此,最终我决定不支持它。在JavaScript中,你无法快速解析完整的语法。你最终会得到比没有它慢10倍或更多的代码。 - vitaly-t
1
@NickeManarin - 你正在使用Typescript,而不是Javascript。这个问题与此无关,但我认为它在抱怨你没有分配类型。例如:(obj: any, path: string) => etc - Nick Grealy
显示剩余4条评论

31

使用 eval:

var part1name = eval("someObject.part1.name");

在发生错误时返回 undefined 进行包装(wrap)

function path(obj, path) {
    try {
        return eval("obj." + path);
    } catch(e) {
        return undefined;
    }
}

http://jsfiddle.net/shanimal/b3xTw/

当使用eval的时候,请谨慎并理性,这有点像使用光剑一样,如果你打开它,有90%的几率会断掉一个手臂。它并不适合所有人。


8
是否使用eval函数是个好主意取决于属性字符串数据的来源。我怀疑你不需要担心黑客通过静态的“var p ='a.b.c'; eval(p);”类型调用来闯入系统。对于这种情况,这是一个完全可行的主意。 - James Wilkins

27

通过以下简单的技巧,您可以使用点符号获取深层对象成员的值,而无需任何外部JavaScript库:

function objectGet(obj, path) { return new Function('_', 'return _.' + path)(obj); };

如果想要从someObject中获取part1.name的值,只需执行以下操作:

objectGet(someObject, 'part1.name');

这里是一个简单的 fiddle 演示:https://jsfiddle.net/harishanchu/oq5esowf/


3
函数 deep_value (obj, path) 的作用是返回对象 obj 中给定路径 path 的深层值。具体实现是通过创建一个新的函数,该函数接受对象 obj 作为参数,并以字符串形式连接路径和对象属性,最后返回所需的值。 - ArcangelZith
看起来很有前途,但它似乎在内部使用了 eval,如果您启用了 CSP 的 unsafe-eval 保护,则会被拒绝。拒绝评估字符串作为 JavaScript,因为 'unsafe-eval' 不是脚本的允许来源 - scipilot

15

这是一行使用 Lodash 的代码。

const deep = { l1: { l2: { l3: "Hello" } } };
const prop = "l1.l2.l3";
const val = _.reduce(prop.split('.'), function(result, value) { return result ? result[value] : undefined; }, deep);
// val === "Hello"

甚至更好的方法是...

const val = _.get(deep, prop);

或使用具有reduce功能的ES6版本...

const val = prop.split('.').reduce((r, val) => { return r ? r[val] : undefined; }, deep);

Plunkr


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