【问题标题】:How to dynamically retrieve arbitrarily specified, deeply nested values from a JavaScript object?如何从 JavaScript 对象中动态检索任意指定的深度嵌套值?
【发布时间】:2021-04-19 03:38:48
【问题描述】:

作为this question 的更正版本重新发布。我已经查看了this,但它并没有帮助我处理未知的数组嵌套。

编辑:添加了有关为什么 Lodash _.get 不起作用的信息。这是Stackblitz。有一个 npm 库,Object-Scan 可以工作——但是,我无法让 Angular 很好地使用那个包。

这适用于 Angular 应用程序中的列渲染器组件。用户选择要显示的列,该列将映射到它的任何位置。因此,在下面的示例中,如果用户选择显示“类别”,并将其映射到“兴趣 [类别]”,则该函数需要在数据中查找与该映射对应的值。

样本数据:

const response = {
  id: "1234",
  version: "0.1",
  drinks: ["Scotch"],
  interests: [
    {
      categories: ["baseball", "football"],
      refreshments: {
        drinks: ["beer", "soft drink"]
      }
    },
    {
      categories: ["movies", "books"],
      refreshments: {
        drinks: ["coffee", "tea", "soft drink"]
      }
    }
  ],
  goals: [
    {
      maxCalories: {
        drinks: "350",
        pizza: "700"
      }
    }
  ]
};

对于这些数据,可能的映射是: id, version, drinks, interests[categories], interests[refreshments][drinks], goals[maxCalories][drinks], goals[maxCalories][pizza],

要求: 我需要一个可以在 Angular 中工作的递归函数,并接受两个参数:一个用于数据对象response,第二个选择器映射字符串将返回适用的、可能嵌套的值,遍历任何对象和/或遇到的数组。映射字符串可能有点或括号表示法。可读的、自记录的代码是非常需要的。

例如,基于上面的数据对象:

  • getValues("id", response); 应该返回 ["1234"]
  • getValues("version", response); 应该返回 ["0.1"]
  • getValues("drinks", response); 应该返回 ["Scotch"]
  • getValues("interests.categories", response) 应该返回 ["baseball", "football", "movies", "books"]
  • getValues("interests[refreshments][drinks]", response); 应该返回 ["beer", "soft drink", "coffee", "tea", "soft drink" ],因为interests 数组中可能有许多项目。
  • getValues("goals.maxCalories.drinks", response); 应该返回 ["350"]

返回的值最终将被删除和排序。

原函数:

function getValues(mapping, row) {
  let rtnValue = mapping
    .replace(/\]/g, "")
    .split("[")
    .map(item => item.split("."))  
    .reduce((arr, next) => [...arr, ...next], [])
    .reduce((obj, key) => obj && obj[key], row)
    .sort();

  rtnValue = [...new Set(rtnValue)]; //dedupe the return values

  return rtnValue.join(", ");
}

上面的工作正常,就像 Lodash 的 get: 如果你通过,例如:"interests[0][refreshments][drinks],它工作得很好。但是,映射不知道嵌套数组,并简单地传入"interests[refreshments][drinks]",从而导致undefined。任何后续的数组项都必须考虑在内。

【问题讨论】:

  • 看来您基本上是在寻找实现 Lodash 的 get function
  • 没有。请参阅我在帖子中添加的说明。
  • getValues("interests[refreshments][drinks]", response); 无效。索引元素只能是数字或带引号的字符串。
  • 你是对的,但这是从数据库返回并传递给函数的字符串。因此,在最初的问题中,replace.split.map.reduce 方法的舞蹈将输入字符串转换为更可接受的内容:["interests", "refreshments", "drinks"] 在下面的答案中,这在单行中处理得更加优雅:query.split (/[[\].]+/g).filter (Boolean)
  • 仍然需要将对象扫描迁移到打字稿,以便 Angular 可以处理它......一旦我得到它可能会在这里发布

标签: javascript arrays angular recursion nested


【解决方案1】:

为什么要重新发明轮子?您所描述的正是 Lodash get 函数所做的。见https://lodash.com/docs/4.17.15#get

如果你真的想要一个“普通”的解决方案,看看他们是怎么做的。见https://github.com/lodash/lodash/blob/master/get.js

【讨论】:

  • Lodash get 需要类似:_.get(response, "interests[0].categories", "default"); 但是,将传递的是:interests.categories(没有数组索引;可以用括号括起来或加点)
【解决方案2】:

更新

在我给出下面的答案后,我吃了一顿晚餐。食物似乎让我意识到我是多么可笑。 My response 对较早的问题显然影响了我对这个问题的看法。但是有一个简单得多的方法,类似于我编写path 函数的方式,只需要少量额外的复杂性来处理数组的映射。这是一种更简洁的方法:

// main function
const _getValues = ([p, ...ps]) => (obj) =>
  p == undefined 
    ? Array.isArray(obj) ? obj : [obj]
  : Array .isArray (obj)
    ?  obj .flatMap (o => _getValues (ps) (o[p] || []))
  : _getValues (ps) (obj [p] || [])


// public function
const getValues = (query, obj) =>
  _getValues (query .split (/[[\].]+/g) .filter (Boolean)) (obj)


// sample data
const response = {id: "1234", version: "0.1", drinks: ["Scotch"], interests: [{categories: ["baseball", "football"], refreshments: {drinks: ["beer", "soft drink"]}}, {categories: ["movies", "books"], refreshments: {drinks: ["coffee", "tea", "soft drink"]}}], goals: [{maxCalories: {drinks: "350", pizza: "700"}}]};


// demo
[
  'id',                               //=> ['1234']
  'version',                          //=> ['0.1']
  'drinks',                           //=> ['Scotch']
  'interests[categories]',            //=> ['baseball', 'football', 'movies', 'books']
  'interests[refreshments][drinks]',  //=> ['beer', 'soft drink', 'coffee', 'tea', 'soft drink']
  'goals.maxCalories.drinks',         //=> ['350']
  'goals.maxCalories.notFound',       //=> []
  'foo.bar.baz',                      //=> []
  'interests[categories][drinks]',    //=> []
] .forEach (
  name => console.log(`"${name}" --> ${JSON.stringify(getValues(name, response))}`)
)
.as-console-wrapper {max-height: 100% !important; top: 0}

这没什么好说的。这是一个足够简单的递归,仍然有一个单独的包装函数来将你的字符串输入格式转换成我觉得更自然地用于这些事情的数组。 (因为这个函数太简单了,所以我内联了之前版本中使用过的name2path函数。

道德

下面的错误版本有一个强烈的寓意:将需求更改作为更改我们的代码的指示太诱人了。我们总是应该尝试将新要求视为重新思考我们的方法的时间,并可能重写我们的代码。当它导致更简单的系统时,这样做是值得的。在这里它显然做到了。

初步回答

(这应该被认为完全被上面的更新所取代。我把它放在这里是为了展示当你有新的需求时忘记从一开始就考虑问题的愚蠢行为。上面的方法非常干净,以至于现在的版本看起来很傻。)

我现在没有太多时间来描述这种技术。 I said 对上一个问题的回答中的大部分内容仍然适用于此代码。 getPaths 被增强了一点,可以返回数组索引的数字节点。

主函数getValues 首先获取当前对象中的所有路径(具有诸如["interests", 0, "categories", 1] 之类的值。)它还将您的查询转换为键,方法是将所有值与'~' 连接起来。然后我们过滤路径以在删除数值时查找哪些与键匹配。同时,我们删除那些以数字结尾的,因为他们的父母会给我们我们需要的所有价值观。然后我们flatMap 调用path 来检索原始对象中每个路径的值。

earlier answer 相当严重地误解了您的要求,但无论如何都提供了一个有趣的功能。我认为这更接近你真正需要的。

// utility functions
const isNumber = (n) =>
  Number(n) === n

const path = (ps) => (obj) =>
  ps .reduce ((o, p) => (o || {}) [p], obj)

const last = (xs) =>
  xs [xs .length - 1]
  
const getPaths = (obj, arr = Array .isArray (obj)) =>
  typeof obj == 'object' 
    ? Object .entries (obj) 
        .flatMap (([k, v]) => [
          [arr ? Number (k): k], 
          ...getPaths (v) .map (p => [arr ? Number (k): k, ...p])
        ])
    : []


// helper function
const name2path = (name) => // probably not a full solutions, but ok for now
  name .split (/[[\].]+/g) .filter (Boolean)


// main function
const getValues = (query, obj, key = name2path (query) .join ('~')) => 
  getPaths (obj)
    .filter (
      p => p.filter(n => ! isNumber (n)) .join ('~') == key && ! isNumber (last (p))
    )
   .flatMap (p => path (p) (obj))


// sample data
const response = {id: "1234", version: "0.1", drinks: ["Scotch"], interests: [{categories: ["baseball", "football"], refreshments: {drinks: ["beer", "soft drink"]}}, {categories: ["movies", "books"], refreshments: {drinks: ["coffee", "tea", "soft drink"]}}], goals: [{maxCalories: {drinks: "350", pizza: "700"}}]};


// demo
[
  'id',
  'version',
  'drinks',
  'interests[categories]',
  'interests[refreshments][drinks]',
  'goals.maxCalories.drinks',
] .forEach (
  name => console.log(`"${name}" --> ${JSON.stringify(getValues(name, response))}`)
)

此方法的一个限制是查询参数不能包含数字索引。这意味着您不能只搜索某个数组的第一个实例。虽然我确信我们可以更改它以允许这样做,但我猜代码会比我们这里的代码复杂得多。

【讨论】:

  • 谢谢,斯科特。正如我们离线讨论的那样,我必须按照MDN Site 中的描述填充 flatMap() 方法。您更新后的解决方案现在运行良好!
【解决方案3】:

使用path-value 语法:

const {tokenizePath, resolveValue} = require('path-value');

function getValues(path, obj) {
    const tokens = tokenizePath(path); // to support full ES5 syntax
    return resolveValue(obj, tokens);
}

getValues("id", response); //=> 1234

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-04-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多