[英]How can I retrieve dynamically specified, arbitrary and deeply nested values from a Javascript object containing strings, objects, and arrays?
UPDATE: While there is good value to the code provided in the answers below, an improved version of this question, and its answer, can be found here .更新:虽然以下答案中提供的代码很有价值,但可以在此处找到此问题的改进版本及其答案。
EDIT: Correcting the sample data object and simplifying (hopefully) the question编辑:更正样本数据 object 并简化(希望)问题
GOAL: Given the below object, a function should parse the object through all its nestings and return the values that correspond to the keypath string argument, which might be a simple string, or include bracketed/dotted notation.目标:给定下面的 object,function 应该通过其所有嵌套解析 object 并返回与键路径字符串参数对应的值,可能包括一个字符串或点符号。 The solution should work in Angular (plain JavaScript, TypeScript, a library that works in Angular).
该解决方案应该在 Angular (纯 JavaScript、TypeScript,一个在 Angular 中工作的库)中工作。
My object:我的 object:
const response = {
"id": "0",
"version": "0.1",
"interests": [ {
"categories": ["baseball", "football"],
"refreshments": {
"drinks": ["beer", "soft drink"],
}
}, {
"categories": ["movies", "books"],
"refreshments": {
"drinks": ["coffee", "tea"]
}
} ],
"goals": [ {
"maxCalories": {
"drinks": "350",
"pizza": "700",
}
} ],
}
The initial function was:最初的 function 是:
function getValues(name, row) {
return name
.replace(/\]/g, '')
.split('[')
.map(item => item.split('.'))
.reduce((arr, next) => [...arr, ...next], [])
.reduce ((obj, key) => obj && obj[key], row);
}
So, if we run getValues("interests[refreshments][drinks]", response);
所以,如果我们运行
getValues("interests[refreshments][drinks]", response);
the function should return an array with all applicable values: ["beer", "soft drink", "coffee", "tea"]. function 应该返回一个包含所有适用值的数组:[“啤酒”、“软饮料”、“咖啡”、“茶”]。
The above works fine for a simple string key.以上适用于简单的字符串键。
getRowValue("version", response)
yields "0.1" as expected. getRowValue("version", response)
按预期产生“0.1”。 But, getRowValue("interests[refreshments][drinks]", response)
returns undefined
.但是,
getRowValue("interests[refreshments][drinks]", response)
返回undefined
。
I crawled through this and the many related links, but am having difficulty understanding how to deal with the complex nature of the object.我浏览了这个和许多相关链接,但很难理解如何处理 object 的复杂性。
Here is a solution using object-scan .这是使用object-scan的解决方案。
The only tricky part is the transformation of the search input into what object-scan expects.唯一棘手的部分是将搜索输入转换为对象扫描所期望的。
// const objectScan = require('object-scan'); const response = { id: '0', version: '0.1', interests: [{ categories: ['baseball', 'football'], refreshments: { drinks: ['beer', 'soft drink'] } }, { categories: ['movies', 'books'], refreshments: { drinks: ['coffee', 'tea'] } }], goals: [{ maxCalories: { drinks: '350', pizza: '700' } }] }; const find = (haystack, needle) => { const r = objectScan( [needle.match(/[^.[\]]+/g).join('.')], { rtn: 'value', useArraySelector: false } )(haystack); return r.length === 1? r[0]: r.reverse(); }; console.log(find(response, 'interests[categories]')); // => [ 'baseball', 'football', 'movies', 'books' ] console.log(find(response, 'interests.refreshments.drinks')); // => [ 'beer', 'soft drink', 'coffee', 'tea' ] console.log(find(response, 'goals[maxCalories][drinks]')); // => 350
.as-console-wrapper {max-height: 100%;important: top: 0}
<script src="https://bundle.run/object-scan@13.8.0"></script>
Disclaimer : I'm the author of object-scan免责声明:我是对象扫描的作者
After thinking about this over night, I've decided that I really don't like the use of coarsen
here.在考虑了一夜之后,我决定我真的不喜欢在这里使用
coarsen
。 (You can see below that I waffled about it in the first place.) Here is an alternative that skips the coarsen
. (您可以在下面看到我一开始对此感到困惑。)这是一个跳过
coarsen
的替代方法。 It does mean that, for instance, passing "id"
will return an array containing that one id, but that makes sense.这确实意味着,例如,传递
"id"
将返回一个包含该 id 的数组,但这是有道理的。 Passing "drinks"
returns an array of drinks, wherever they are found.传递
"drinks"
会返回一系列饮料,无论它们在哪里。 A consistent interface is much cleaner.一致的界面更加简洁。 All the discussion about this below (except for
coarsen
) still applies.下面所有关于这个的讨论(除了
coarsen
)仍然适用。
// utility functions const last = (xs) => xs [xs.length - 1] const endsWith = (x) => (xs) => last(xs) == x const path = (ps) => (obj) => ps.reduce ((o, p) => (o || {}) [p], obj) const getPaths = (obj) => typeof obj == 'object'? Object.entries (obj).flatMap (([k, v]) => [ [k], ...getPaths (v).map (p => [k, ...p]) ]): [] const hasSubseq = ([x, ...xs]) => ([y, ...ys]) => y == undefined? x == undefined: xs.length > ys.length? false: x == y? hasSubseq (xs) (ys): hasSubseq ([x, ...xs]) (ys) // helper functions const findPartialMatches = (p, obj) => getPaths (obj).filter (endsWith (last (p))).filter (hasSubseq (p)).flatMap (p => path (p) (obj)) const name2path = (name) => // probably not a full solutions, but ok for now name.split (/[[\].]+/g).filter (Boolean) // main function const newGetRowValue = (name, obj) => findPartialMatches (name2path (name), obj) // sample data let response = {id: "0", version: "0.1", interests: [{categories: ["baseball", "football"], refreshments: {drinks: ["beer", "soft drink"]}}, {categories: ["movies", "books"], refreshments: {drinks: ["coffee", "tea"]}}], goals: [{maxCalories: {drinks: "350", pizza: "700"}}]}; // demo [ 'interests[refreshments].drinks', 'interests[drinks]', 'drinks', 'interests[categories]', 'goals', 'id', 'goals.maxCalories', 'goals.drinks' ].forEach ( name => console.log(`"${name}" --> ${JSON.stringify(newGetRowValue(name, response))}`) )
.as-console-wrapper {max-height: 100%;important: top: 0}
I still have some questions about your requirements.我仍然对您的要求有一些疑问。 See my comment on the question for details.
有关详细信息,请参阅 我对问题的评论。 I'm making an assumption here that your requirements are slightly more consistent than suggested: mostly that the nodes in your name must be present, and the nesting structure must be as indicated, but that there might be intermediate nodes not mentioned.
我在这里假设您的要求比建议的稍微一致:主要是您名称中的节点必须存在,并且嵌套结构必须如指示的那样,但可能存在未提及的中间节点。 Thus
"interests.drinks"
would include the values of both interests[0].drinks
and "interests[1].refreshments.drinks"
, but not of "goals.maxCategories.drinks"
, since that does not include any "interests"
node.因此
"interests.drinks"
将包括interests[0].drinks
和"interests[1].refreshments.drinks"
的值,但不包括"goals.maxCategories.drinks"
的值,因为它不包括任何"interests"
节点。
This answer also has a bit of a hack: the basic code would return an array for any input.这个答案也有一点技巧:基本代码将为任何输入返回一个数组。 But there are times when that array has only a single value, and usually we would want to return just that value.
但有时该数组只有一个值,通常我们只想返回那个值。 That is the point of the
coarsen
function used in findPartialMatches
.这就是 findPartialMatches 中使用的
coarsen
findPartialMatches
的要点。 It's an ugly hack, and if you can live with id
yielding ["0"]
in an array, I would remove the call to coarsen
.这是一个丑陋的黑客,如果你可以忍受
id
在数组中产生["0"]
,我会删除对coarsen
的调用。
Most of the work here uses arrays for the path rather than your name value.这里的大部分工作使用 arrays 作为路径而不是您的名称值。 I find it much simpler, and simply convert to that format before doing anything substantial.
我发现它更简单,并且在做任何实质性的事情之前只需转换为那种格式。
Here is an implementation of this idea:这是这个想法的一个实现:
// utility functions const last = (xs) => xs [xs.length - 1] const endsWith = (x) => (xs) => last(xs) == x const path = (ps) => (obj) => ps.reduce ((o, p) => (o || {}) [p], obj) const getPaths = (obj) => typeof obj == 'object'? Object.entries (obj).flatMap (([k, v]) => [ [k], ...getPaths (v).map (p => [k, ...p]) ]): [] const hasSubseq = ([x, ...xs]) => ([y, ...ys]) => y == undefined? x == undefined: xs.length > ys.length? false: x == y? hasSubseq (xs) (ys): hasSubseq ([x, ...xs]) (ys) // helper functions const coarsen = (xs) => xs.length == 1? xs[0]: xs const findPartialMatches = (p, obj) => coarsen (getPaths (obj).filter (endsWith (last (p))).filter (hasSubseq (p)).flatMap (p => path (p) (obj)) ) const name2path = (name) => // probably not a full solutions, but ok for now name.split (/[[\].]+/g).filter (Boolean) // main function const newGetRowValue = (name, obj) => findPartialMatches (name2path (name), obj) // sample data let response = {id: "0", version: "0.1", interests: [{categories: ["baseball", "football"], refreshments: {drinks: ["beer", "soft drink"]}}, {categories: ["movies", "books"], refreshments: {drinks: ["coffee", "tea"]}}], goals: [{maxCalories: {drinks: "350", pizza: "700"}}]}; // demo [ 'interests[refreshments].drinks', 'interests[drinks]', 'drinks', 'interests[categories]', 'goals', 'id', 'goals.maxCalories', 'goals.drinks' ].forEach ( name => console.log(`"${name}" --> ${JSON.stringify(newGetRowValue(name, response))}`) )
.as-console-wrapper {max-height: 100%;important: top: 0}
We start with two simple utility functions:我们从两个简单的实用函数开始:
last
returns the last element of an array last
返回数组的最后一个元素
endsWith
simply reports if the last element of an array equals a test value endsWith
仅报告数组的最后一个元素是否等于测试值
Then a few more substantial utility functions:然后是一些更重要的实用功能:
path
takes an array of node names, and an object and finds the value of at that node path in an object. path
采用节点名称数组和 object 并在 object 中找到该节点路径的值。
getPaths
takes an object and returns all the paths found in it. getPaths
采用 object 并返回在其中找到的所有路径。 For instance, the sample object will yield something like this:例如,样本 object 将产生如下内容:
[ ["id"], ["version"], ["interests"], ["interests", "0"], ["interests", "0", "categories"], ["interests", "0", "categories", "0"], ["interests", "0", "categories", "1"], ["interests", "0", "drinks"], //... ["goals"], ["goals", "0"], ["goals", "0", "maxCalories"], ["goals", "0", "maxCalories", "drinks"], ["goals", "0", "maxCalories", "pizza"] ]
hasSubseq
reports whether the elements of the first argument can be found in order within the second one. hasSubseq
报告是否可以在第二个参数中按顺序找到第一个参数的元素。 Thus hasSubseq ([1, 3]) ([1, 2, 3, 4)
returns true
, but hasSubseq ([3, 1]) ([1, 2, 3, 4)
returns false
.因此
hasSubseq ([1, 3]) ([1, 2, 3, 4)
返回true
,但hasSubseq ([3, 1]) ([1, 2, 3, 4)
返回false
。 (Note that this implementation was thrown together without a great deal of thought. It might not work properly, or it might be less efficient than necessary.) (请注意,这个实现是在没有经过深思熟虑的情况下拼凑起来的。它可能无法正常工作,或者效率可能比必要的低。)
After that we have three helper functions.之后,我们有了三个辅助函数。 (I distinguish utility functions from helper functions this way: utility functions may be useful in many places in the project and even across projects. Helper functions are specific to the problem at hand.):
(我以这种方式区分实用程序函数和辅助函数:实用程序函数可能在项目的许多地方甚至跨项目中都有用。辅助函数特定于手头的问题。):
coarsen
was discussed above and it simply turns single-element arrays into scalar values.上面讨论了
coarsen
,它只是将单个元素 arrays 转换为标量值。 There's a good argument for removing this altogether.有一个很好的论据可以完全删除它。
findPartialMatches
is central. findPartialMatches
是中心。 It does what our main function is designed to do, but using an array of node names rather than a dot/bracket-separated string.它完成了我们的主要 function 的设计目的,但使用节点名称数组而不是点/括号分隔的字符串。
name2path
converts the dot/bracket-separated string into an array. name2path
将点/括号分隔的字符串转换为数组。 I would move this up to the utility section, except that I'm afraid that it may not be as robust as we would like.我会将其移至实用程序部分,但我担心它可能不像我们希望的那样强大。
And finally, the main function simply calls findPartialMatches
using the result of name2path
on the name
parameter.最后,主要的 function 只需在
name
参数上使用name2path
的结果调用findPartialMatches
。
The interesting code is findPartialMatches
, which gets all the paths in the object, and then filters the list to those that end with the last node of our path, then further filters these to the ones that have our path as a subsequence, retrieves the values at each of these paths, wraps them in an array, and then calls the unfortunate coarsen
on this result.有趣的代码是
findPartialMatches
,它获取 object 中的所有路径,然后将列表过滤到以我们路径的最后一个节点结尾的那些,然后将这些进一步过滤到以我们的路径作为子序列的那些,检索值在这些路径中的每一个,将它们包装在一个数组中,然后在这个结果上调用不幸的coarsen
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.