通过字符串路径访问嵌套的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个回答

12

我认为您在询问这个:

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

你可能在询问这个:

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

这两种方法都可以使用。


或者也许你是在问这个。

var partName = "part1";
var nameStr = "name";

var part1name = someObject[partName][nameStr];
最后,你可能会要求这个。
var partName = "part1.name";

var partBits = partName.split(".");

var part1name = someObject[partBits[0]][partBits[1]];

我认为OP正在寻找最后的解决方案。然而,字符串没有Split方法,而是split - duri
实际上,我是在问最后一个问题。partName变量填充了一个字符串,指示键结构到值的映射关系。你的解决方案似乎很有道理。但是,我可能需要修改以处理更深层次的数据,例如4-5级及以上。我想知道是否可以用同样的方法来统一处理数组和对象? - Komaruloh

10

不要试图模仿JS语法,因为你将不得不花费大量计算来解析,或者像一些答案(带有 . 的键,任何人都会忘记吗?)那样犯错/忘记,而是使用一组键。

var part1name     = Object.get(someObject, ['part1', 'name']);
var part2quantity = Object.get(someObject, ['part2', 'qty']);
var part3name1    = Object.get(someObject, ['part3', 0, 'name']);

answer

如果您需要使用单个字符串,则只需将其JSON化。
此方法的另一个改进是您可以删除/设置根级对象。

function resolve(obj, path) {
    let root = obj = [obj];
    path = [0, ...path];
    while (path.length > 1)
        obj = obj[path.shift()];
    return [obj, path[0], root];
}
Object.get = (obj, path) => {
    let [parent, key] = resolve(obj, path);
    return parent[key];
};
Object.del = (obj, path) => {
    let [parent, key, root] = resolve(obj, path);
    delete parent[key];
    return root[0];
};
Object.set = (obj, path, value) => {
    let [parent, key, root] = resolve(obj, path);
    parent[key] = value;
    return root[0];
};

其他功能的演示:
演示

bob = 用于.set(/.del(语句只有在路径可能为空(操作根对象)时才是必要的。
我通过使用steve来保留对原始对象的引用,并在第一个.set(后检查bob == steve //true来证明我没有克隆对象。


使用“Object.get”时出现以下错误:“路径不可迭代”。 - Nikk
3
在我的情况下,我收到了“Object.get()不是一个函数”的错误。 - huntzinger92

9
如果有人在2017年或之后访问这个问题,并且正在寻找一个易于记忆的方法,那么这里有一篇详细的博文JavaScript中访问嵌套对象,可以避免被搞糊涂。 无法读取未定义的属性'foo'错误

使用数组Reduce访问嵌套对象

让我们以这个例子结构为例。
const user = {
    id: 101,
    email: 'jack@dev.com',
    personalInfo: {
        name: 'Jack',
        address: [{
            line1: 'westwish st',
            line2: 'washmasher',
            city: 'wallas',
            state: 'WX'
        }]
    }
}

为了能够访问嵌套数组,你可以编写自己的数组减少工具。
const getNestedObject = (nestedObj, pathArr) => {
    return pathArr.reduce((obj, key) =>
        (obj && obj[key] !== undefined) ? obj[key] : undefined, nestedObj);
}

// pass in your object structure as array elements
const name = getNestedObject(user, ['personalInfo', 'name']);

// to access nested array, just pass in array index as an element the path array.
const city = getNestedObject(user, ['personalInfo', 'address', 0, 'city']);
// this will return the city from the first address item.

还有一个优秀的类型处理最小化库typy,它可以为您完成所有这些工作。
使用typy,您的代码将如下所示。
const city = t(user, 'personalInfo.address[0].city').safeObject;

免责声明:本人为本软件的作者。

1
我认为你的意思是 obj && obj[key] !== undefined 而不是 obj && obj[key] !== 'undefined' - nzn

7

我在这里提供更多方法,这些方法在许多方面看起来更快:

选项1:根据 . 或 [ 或 ] 或 ' 或 " 将字符串分割,反转它,跳过空项。

function getValue(path, origin) {
    if (origin === void 0 || origin === null) origin = self ? self : this;
    if (typeof path !== 'string') path = '' + path;
    var parts = path.split(/\[|\]|\.|'|"/g).reverse(), name; // (why reverse? because it's usually faster to pop off the end of an array)
    while (parts.length) { name=parts.pop(); if (name) origin=origin[name]; }
    return origin;
}

选项2(除了eval之外最快的选项):低级字符扫描(无正则表达式/分割等,只是快速字符扫描)。 注意:此选项不支持索引中的引号。
function getValue(path, origin) {
    if (origin === void 0 || origin === null) origin = self ? self : this;
    if (typeof path !== 'string') path = '' + path;
    var c = '', pc, i = 0, n = path.length, name = '';
    if (n) while (i<=n) ((c = path[i++]) == '.' || c == '[' || c == ']' || c == void 0) ? (name?(origin = origin[name], name = ''):(pc=='.'||pc=='['||pc==']'&&c==']'?i=n+2:void 0),pc=c) : name += c;
    if (i==n+2) throw "Invalid path: "+path;
    return origin;
} // (around 1,000,000+/- ops/sec)

选项3::选项2扩展以支持引号 - 速度略慢,但仍然很快)
function getValue(path, origin) {
    if (origin === void 0 || origin === null) origin = self ? self : this;
    if (typeof path !== 'string') path = '' + path;
    var c, pc, i = 0, n = path.length, name = '', q;
    while (i<=n)
        ((c = path[i++]) == '.' || c == '[' || c == ']' || c == "'" || c == '"' || c == void 0) ? (c==q&&path[i]==']'?q='':q?name+=c:name?(origin?origin=origin[name]:i=n+2,name='') : (pc=='['&&(c=='"'||c=="'")?q=c:pc=='.'||pc=='['||pc==']'&&c==']'||pc=='"'||pc=="'"?i=n+2:void 0), pc=c) : name += c;
    if (i==n+2 || name) throw "Invalid path: "+path;
    return origin;
}

JSPerf: http://jsperf.com/ways-to-dereference-a-delimited-property-string/3

在性能方面,"eval(...)"仍然是最好的选择。如果您直接控制属性路径,则使用'eval'不会有任何问题(特别是如果需要速度)。如果要通过网络获取属性路径(在线!?哈哈 :P),那么请使用其他安全的方法。只有白痴才会说根本不要使用"eval",因为有时候确实需要使用它。此外,"它被用于Doug Crockford的JSON解析器。" 如果输入是安全的,那么就没有问题了。用正确的工具做正确的事情,就这样。


7

AngularJS

Speigg的方法非常简洁清晰,但是我在搜索访问AngularJS $scope属性的字符串路径的解决方案时发现了这个回复,并进行了一些修改,它可以胜任:

$scope.resolve = function( path, obj ) {
    return path.split('.').reduce( function( prev, curr ) {
        return prev[curr];
    }, obj || this );
}

只需将此函数放在您的根控制器中,并在任何子作用域中使用它,如下所示:

$scope.resolve( 'path.to.any.object.in.scope')

请参考AngularJS中的$scope.$eval了解另一种使用AngularJS实现的方法。 - georgeawg

5
如果你需要一种能够正确检测和报告路径解析问题详细信息的解决方案,我自己写了一个解决方案 - path-value 库。
const {resolveValue} = require('path-value');

resolveValue(someObject, 'part1.name'); //=> Part 1
resolveValue(someObject, 'part2.qty'); //=> 50
resolveValue(someObject, 'part3.0.name'); //=> Part 3A

请注意,对于索引值我们使用.0而不是[0],因为解析后者会导致性能下降,而.0 直接在JavaScript中运行,因此非常快。
然而,完整的ES5 JavaScript语法也得到支持,只需要先进行令牌化处理。
const {resolveValue, tokenizePath} = require('path-value');

const path = tokenizePath('part3[0].name'); //=> ['part3', '0', 'name']

resolveValue(someObject, path); //=> Part 3A

4
/**
 * Access a deep value inside a object 
 * Works by passing a path like "foo.bar", also works with nested arrays like "foo[0][1].baz"
 * @author Victor B. https://gist.github.com/victornpb/4c7882c1b9d36292308e
 * Unit tests: http://jsfiddle.net/Victornpb/0u1qygrh/
 */
function getDeepVal(obj, path) {
    if (typeof obj === "undefined" || obj === null) return;
    path = path.split(/[\.\[\]\"\']{1,2}/);
    for (var i = 0, l = path.length; i < l; i++) {
        if (path[i] === "") continue;
        obj = obj[path[i]];
        if (typeof obj === "undefined" || obj === null) return;
    }
    return obj;
}

与之配合使用

getDeepVal(obj,'foo.bar')
getDeepVal(obj,'foo.1.bar')
getDeepVal(obj,'foo[0].baz')
getDeepVal(obj,'foo[1][2]')
getDeepVal(obj,"foo['bar'].baz")
getDeepVal(obj,"foo['bar']['baz']")
getDeepVal(obj,"foo.bar.0.baz[1]['2']['w'].aaa[\"f\"].bb")

3

我尚未找到一个可用于执行所有字符串路径操作的软件包,因此我最终编写了自己的快速小型软件包,支持insert()、get()(带默认返回值)、set()和remove()操作。

您可以使用点符号表示法、括号、数字索引、字符串数字属性和带有非单词字符的键。以下是简单的用法:

> var jsocrud = require('jsocrud');

...

// Get (Read) ---
> var obj = {
>     foo: [
>         {
>             'key w/ non-word chars': 'bar'
>         }
>     ]
> };
undefined

> jsocrud.get(obj, '.foo[0]["key w/ non-word chars"]');
'bar'

https://www.npmjs.com/package/jsocrud

https://github.com/vertical-knowledge/jsocrud


3

这是一个简单函数,允许使用字符串或数组路径。

function get(obj, path) {
  if(typeof path === 'string') path = path.split('.');

  if(path.length === 0) return obj;
  return get(obj[path[0]], path.slice(1));
}

const obj = {a: {b: {c: 'foo'}}};

console.log(get(obj, 'a.b.c')); //foo

或者

console.log(get(obj, ['a', 'b', 'c'])); //foo

如果您要将代码作为答案发布,请解释代码为什么回答了问题。 - Tieson T.

2
虽然reduce是很好的,但我很惊讶没有人使用forEach:
function valueForKeyPath(obj, path){
        const keys = path.split('.');
        keys.forEach((key)=> obj = obj[key]);
        return obj;
    };

Test


你甚至没有检查 obj[key] 是否真的存在。这是不可靠的。 - Neithan Max
默认情况下,JavaScript 不会检查对象的键是否存在:如果您的对象中没有属性 b,则 a.b.c 会引发异常。如果您需要静默地忽略错误的键路径(我不建议这样做),您仍然可以使用以下代码替换 forEach:keys.forEach((key)=> obj = (obj||{})[key]); - Flavien Volken
我运行了一个缺少花括号的对象,是我的错误 :) - Neithan Max

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