简体   繁体   中英

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

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. The solution should work in Angular (plain JavaScript, TypeScript, a library that works in Angular).

My 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 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); the function should return an array with all applicable values: ["beer", "soft drink", "coffee", "tea"].

The above works fine for a simple string key. getRowValue("version", response) yields "0.1" as expected. But, getRowValue("interests[refreshments][drinks]", response) returns 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.

Here is a solution using 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

Update

After thinking about this over night, I've decided that I really don't like the use of coarsen here. (You can see below that I waffled about it in the first place.) Here is an alternative that skips the coarsen . It does mean that, for instance, passing "id" will return an array containing that one id, but that makes sense. Passing "drinks" returns an array of drinks, wherever they are found. A consistent interface is much cleaner. All the discussion about this below (except for coarsen ) still applies.

 // 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}

Original Answer

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.

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 . It's an ugly hack, and if you can live with id yielding ["0"] in an array, I would remove the call to coarsen .

Most of the work here uses arrays for the path rather than your name value. 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

  • endsWith simply reports if the last element of an array equals a test value

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.

  • getPaths takes an object and returns all the paths found in it. For instance, the sample object will yield something like this:

     [ ["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. Thus hasSubseq ([1, 3]) ([1, 2, 3, 4) returns true , but hasSubseq ([3, 1]) ([1, 2, 3, 4) returns 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. There's a good argument for removing this altogether.

  • findPartialMatches is central. It does what our main function is designed to do, but using an array of node names rather than a dot/bracket-separated string.

  • name2path converts the dot/bracket-separated string into an array. 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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM