Typescript - 在多维对象中查找匹配的键和路径 [英] Typescript - Finding matching keys and paths in a multidimensional object

查看:44
本文介绍了Typescript - 在多维对象中查找匹配的键和路径的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

简而言之;我如何能够在多个对象中检索共享密钥的所有值?

更长:我得到了以下对象,它们存储在一个名为 collection 的数组中:

<预><代码>[名称:{一":正方形";二": {三":圈"四":{五":三角形"}}}形状:{一": "[]";二": {三": "()"四":{五": "/\"}}}]

我在 Angular(v8.x.x) 中创建了一个菜单系统,它从 names 对象中读取键.当我点击圆圈"的菜单项时,我希望获得密钥&值配对它们的路径以在编辑窗口中使用.每个项目都需要这样做.例如:

onClick(menuItem){const 路径 = collection.search(menuItem.key)console.log(paths) \\ 期望:["names.two.three", "shapes.two.three"]打开编辑器(路径)}打开编辑器(路径){for(路径中的路径){显示.名称(路径.key)inputfield.value(path.value)}

|----------|三:|圈|-----------|三:|()|-----------

我尝试自己创建一个递归函数,但到目前为止还没有取得任何可行的结果.我也尝试过 Scott 的惊人示例,尽管 angular/typescript 不幸地在 assocPath() 中对 ps 的定义抛出了一个错误:

any[]"类型的参数不可分配给[any, ...any[]]"类型的参数.类型any[]"中缺少属性0",但类型[any, ...any[]]"中需要属性0".

此外,我也看过这些答案:

1:StackOverflow:Javascript/JSON 获取给定子节点的路径?

2:StackOverflow: 在 JavaScript 中获取 JSON 对象的路径"

第一个有一个错误,关于 path 是在声明之前使用的块范围变量,我目前正在对我的场景中的第二个进行故障排除.

解决方案

更新

当我最初编写此代码时,我显然是从其他地方剪切和粘贴了一些代码,因为我包含了两个根本没有使用的函数.它们现在已被移除.

我还在下面添加了一个解释,说明我如何逐步将这些函数之一从 ES5 样式转换为答案中的现代 JS 版本.我相信 ES6+ 是一个重大改进,我更喜欢我写的函数,但对于那些学习现代 JS 的人来说,这样的解释可能有用.

更新后的原始答案如下,然后是这些转换步骤.

<小时>

我真的不清楚你在找什么.我最好的猜测是你想要接受一个像上面这样的对象数组并返回一个函数,该函数接受一个类似 "circle""()" 的值并返回这些对象之一的该值的路径,即 ['two', 'three'].但这种猜测可能会落空.

这是一个基于一些可重用功能的版本:

//Helpersconst path = (ps = [], obj = {}) =>ps .reduce ((o, p) => (o || {}) [p], obj)const findLeafPaths = (o, path = [[]]) =>typeof o == '对象'?对象 .entries (o) .flatMap (([k, v]) =>findLeafPaths (v, path).map(p => [k, ...p])): 小路//主功能const makeSearcher = (xs) =>{const 结构 = xs .reduce ((a, x) =>findLeafPaths (x) .reduce ((a, p) => ({...a, [path (p, x)]: p}), a),{})返回 (val) =>结构[val] ||[]//还有什么?或抛出错误?}//演示常量对象 = [{一:正方形",二:{三:圆",四:{五:三角形"}}},{一二三四五: "/\\"}}}]const searcher = makeSearcher(objs)控制台 .log (searcher ('()'))//~>['二三']控制台 .log (searcher ('circle'))//~>['二三']控制台 .log (searcher ('triangle'))//~>['二'、四'、'五']控制台 .log (searcher ('[]'))//~>['一']控制台 .log (searcher ('heptagon'))//~>[]

我们从两个辅助函数开始,pathfindLeafPaths.这些都是可重用的功能.第一个从 Ramda 借用了它的 API,尽管这是一个单独的实现:

  • path 接受一个节点列表(例如 ['two', 'three'])和一个对象,并返回该路径的值(如果所有)沿途节点存在

  • findLeafPaths 获取一个对象并将其视为一棵树,返回所有叶节点的路径.因此,对于您的第一个对象,它将返回 [['one'], ['two', 'three'], ['two', 'four', 'five']].我们再次忽略了数组,我什至不确定我们需要做什么来支持它们.

主要功能是makeSearcher.它需要一个像这样的对象数组:

[{一:正方形",二:{三:圆",四:{五:三角形"}}},{一二三四五: "/\\"}}}]

并将它们转换成如下所示的结构:

{'正方形':['一个']'圆圈':['二','三']'三角形':['二'、'四'、'五']'[]'       : ['一']'()'       : ['二三']'/\\' : ['二', '四', '五']}

然后返回一个简单地从这个结构中查找值的函数.

我有一些模糊的怀疑,认为这段代码没有我喜欢的那么深思熟虑,因为我找不到比结构"更好的帮助对象名称.欢迎提出任何建议.

将 ES5 转换为现代 JS

这里我们展示了一系列从 ES5 到现代 Javascript 代码的转换.请注意,我实际上是按照其他顺序编写这些内容的,因为在使用 ES6+ 几年后,我现在自然而然地想到了它.不过,这可能对那些来自 ES5 背景的人有所帮助.

我们将转换一个版本的 findLeafPaths.这是我认为跳过所有 ES6+ 功能的版本:

const findLeafPaths = function (o, path) {如果(typeof o == '对象'){const 键 = 对象 .keys (o)const 条目 = 键 .map (key => [key, o [key]])const partialPaths = 条目 .map ( 函数 ([k, v]) {const 路径 = findLeafPaths (v, path || [[]])返回路径 .map (function(p) {返回 [k].concat(p)})})返回 partialPaths.reduce(function(a, b) {返回 a.concat(b)}, [])}返回路径 ||[[]]}

我们做的第一件事是使用 Object.entries 用获取对象的键然后映射这些键来获取 [key, value] 对来替换舞蹈:

const findLeafPaths = function (o, path) {如果(typeof o == '对象'){const 条目 = 对象 .entries (o)const partialPaths = 条目 .map (function ([k, v]) {const 路径 = findLeafPaths (v, path || [[]])返回路径 .map (function(p) {返回 [k] .concat (p)})})return partialPaths.reduce(function (a, b) {返回一个 .concat (b)}, [])}返回路径 ||[[]]}

接下来,映射的模式,然后通过连接减少来扁平化有一个内置的数组方法,flatMap.我们可以通过使用它来简化:

const findLeafPaths = function (o, path) {如果(typeof o == '对象'){const 条目 = 对象 .entries (o)返回条目 .flatMap (function ([k, v]) {const 路径 = findLeafPaths (v, path || [[]])返回路径 .map (function(p) {返回 [k] .concat (p)})})}返回路径 ||[[]]}

现在我们可以调整它以利用现代 spread 语法代替 concat:

const findLeafPaths = function (o, path) {如果(typeof o == '对象'){const 条目 = 对象 .entries (o)返回条目 .flatMap ( function ([k, v]) {const 路径 = findLeafPaths (v, path || [[]])返回路径 .map (function(p) {返回 [k, ...p]})})}返回路径 ||[[]]}

箭头函数将简化事情更多.这里我们用箭头替换最里面的函数调用:

const findLeafPaths = function (o, path) {如果(typeof o == '对象'){const 条目 = 对象 .entries (o)返回条目 .flatMap ( function ([k, v]) {const 路径 = findLeafPaths (v, path || [[]])返回路径 .map (p => [k, ...p])})}返回路径 ||[[]]}

我们在重复 path ||[[]] 表达式在两处.我们可以使用 默认参数有一个:

const findLeafPaths = function (o, path = [[]]) {如果(typeof o == '对象'){const 条目 = 对象 .entries (o)返回条目 .flatMap ( function ([k, v]) {返回 findLeafPaths (v, path) .map (p => [k, ...p])})}返回路径}

现在我们用箭头替换下一个函数表达式(提供给 entries.flatmap()):

const findLeafPaths = function (o, path = [[]]) {如果(typeof o == '对象'){const 条目 = 对象 .entries (o)返回条目 .flatMap (([k, v]) =>findLeafPaths (v, path) .map (p => [k, ...p]))}返回路径}

entries 是一个临时变量,我们在定义后的行中只使用一次.我们可以轻松删除它:

const findLeafPaths = function (o, path = [[]]) {如果(typeof o == '对象'){返回对象 .entries (o) .flatMap (([k, v]) =>findLeafPaths (v, path) .map (p => [k, ...p]))}返回路径}

从函数的角度来看,使用表达式比使用语句更可取.它们更容易受到分析,并且不依赖于外部排序.因此,我会为 if-else 选择一个条件表达式(三元语句").所以我更喜欢这个版本:

const findLeafPaths = function (o, path = [[]]) {返回 typeof o == 'object'?对象 .entries (o) .flatMap (([k, v]) =>findLeafPaths (v, path) .map (p => [k, ...p])): 小路}

最后,我们可以用另一个箭头函数替换最外层的函数表达式以获得上面答案中的版本:

const findLeafPaths = (o, path = [[]]) =>typeof o == '对象'?对象 .entries (o) .flatMap (([k, v]) =>findLeafPaths (v, path) .map (p => [k, ...p])): 小路

显然我们也可以用 pathmakeSearcher 做同样的事情.

注意,此操作的每一步都会减少函数的行数或字符数.这很好,但这根本不是最重要的一点.更相关的是,每个版本都可以说比之前的版本更简单.这并不意味着它更熟悉,只是更少的想法被缠绕在一起.(Rich Hickey 的Simple Made Easy解释这些经常混淆的概念之间的区别的好主意.)

我经常与初级开发人员一起工作,让他们完成这种转变对于他们技能的增长很重要.那里没有困难的步骤,但最终结果比原来简单得多.一段时间后,直接以这种风格写作可能会成为第二天性.

In short; How would I be able to retrieve all values of a shared key in multiple objects?

Longer: I've been given the following objects, stored in an array called collection:

[
    names: {
        "one": "square";
        "two": {
            "three": "circle"
            "four": {
                "five": "triangle"
                }
            }
        }
    shapes: {
        "one": "[]";
        "two": {
            "three": "()"
            "four": {
                "five": "/\"
                }
            }
        }
]

I've made a menu system in Angular(v8.x.x) that reads the keys from the names objects. When I click on the menu item for "circle", I hope to obtain the key & value pair their paths for use in an editing window. This would need to happen for each item. Ex:

onClick(menuItem){
    const paths = collection.search(menuItem.key)
    console.log(paths) \\ expecting: ["names.two.three", "shapes.two.three"]
    openEditor(paths)
}

openEditor(paths){
    for(path in paths){
        display.Name(path.key)
        inputfield.value(path.value)
    }

|----------
|three: 
|circle
|----------
|three:
|()
|----------

I've attempted to create a recursive function myself but so far haven't achieved any feasible result. I have also tried Scott's amazing examples, although angular/typescript unfortunately throws an error on the definition of ps in assocPath():

Argument of type 'any[]' is not assignable to parameter of type '[any, ...any[]]'.
      Property '0' is missing in type 'any[]' but required in type '[any, ...any[]]'.

Additionally, I have looked to these answers as well:

1: StackOverflow: Javascript/JSON get path to given subnode?

2: StackOverflow: Get the "path" of a JSON object in JavaScript

The 1st has an error regarding path being a block-scoped variable being used before its declaration and I'm currently troubleshooting the 2nd in my scenario.

解决方案

Update

I clearly was cutting and pasting some code from elsewhere when I originally wrote this, as I included two functions that weren't being used at all. They're removed now.

I also added below an explanation of how I might, step-by-step, convert one of these functions from an ES5 style to the modern JS version in the answer. I believe ES6+ is a major improvement, and I like the function as I wrote it much better, but for those learning modern JS, such an explanation might be useful.

The updated original answer follows and then these transformation steps.


It's really unclear to me what you're looking for. My best guess is that you want to accept an array of objects like the above and return a function that takes a value like "circle" or "()" and returns the path to that value on one of those objects, namely ['two', 'three']. But that guess could be way off.

Here's a version that does this based on a few reusable functions:

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

const findLeafPaths = (o, path = [[]]) => 
  typeof o == 'object'
    ? Object .entries (o) .flatMap (
        ([k, v]) => findLeafPaths (v, path).map(p => [k, ...p])
      ) 
    : path


// Main function
const makeSearcher = (xs) => {
  const structure = xs .reduce (
    (a, x) => findLeafPaths (x) .reduce ((a, p) => ({...a, [path (p, x)]: p}), a),
    {}
  )
  return (val) => structure[val] || [] // something else? or throw error?
}


// Demonstration
const objs = [
  {one: "square", two: {three: "circle", four: {five: "triangle"}}}, 
  {one: "[]", two: {three: "()", four: {five: "/\\"}}}
]

const searcher = makeSearcher(objs)

console .log (searcher ('()'))        //~> ['two', 'three']
console .log (searcher ('circle'))    //~> ['two', 'three']
console .log (searcher ('triangle'))  //~> ['two', four', 'five']
console .log (searcher ('[]'))        //~> ['one']
console .log (searcher ('heptagon'))  //~> []
        

We start with two helper functions, path, and findLeafPaths. These are all reusable functions. The first borrows its API from Ramda, although this is a separate implementation:

  • path accepts a list of nodes (e.g. ['two', 'three']) and an object and returns the value at that path if all the nodes along the way exist

  • findLeafPaths takes an object and viewing it as a tree, returns the paths of all leaf nodes. Thus for your first object, it would return [['one'], ['two', 'three'], ['two', 'four', 'five']]. Again we ignore arrays, and I'm not even sure what we would need to do to support them.

The main function is makeSearcher. It takes an array of objects like this:

[
  {one: "square", two: {three: "circle", four: {five: "triangle"}}}, 
  {one: "[]", two: {three: "()", four: {five: "/\\"}}}
]

and converts it them into a structure that looks like this:

{
  'square'   : ['one']
  'circle'   : ['two', 'three']
  'triangle' : ['two', 'four', 'five']
  '[]'       : ['one']
  '()'       : ['two', 'three']
  '/\\'      : ['two', 'four', 'five']
}

and then returns a function that simply looks up the values from this structure.

I have some vague suspicions that this code is not quite as well thought-out as I like, since I can't find a better name for the helper object than "structure". Any suggestions would be welcome.

Transforming ES5 to modern JS

Here we show a series of transformations from ES5 to modern Javascript code. Note that I actually wrote these in the other order, as the ES6+ is now what come naturally to me after working in it for a few years. This may be helpful for those coming from ES5 backgrounds, though.

We're going to convert a version of findLeafPaths. Here is a version that I think skips all ES6+ features:

const findLeafPaths = function (o, path) {
  if (typeof o == 'object') {
    const keys = Object .keys (o)
    const entries = keys .map (key => [key, o [key]])
    const partialPaths = entries .map ( function ([k, v]) {
      const paths = findLeafPaths (v, path || [[]])
      return paths .map (function(p) {
        return [k].concat(p)
      })
    })
    return partialPaths.reduce(function(a, b) {
      return a.concat(b)
    }, [])
  }
  return path || [[]]
}

The first thing we do is use Object.entries to replace the dance with getting the object's keys and then mapping those to get the [key, value] pairs:

const findLeafPaths = function (o, path) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    const partialPaths = entries .map (function ([k, v]) {
      const paths = findLeafPaths (v, path || [[]])
      return paths .map (function(p) {
        return [k] .concat (p)
      })
    })
    return partialPaths.reduce(function (a, b) {
      return a .concat (b)
    }, [])
  }
  return path || [[]]
}

Next, the pattern of mapping, then flattening by reducing with concatenation has a a built-in Array method, flatMap. We can simplify by using that:

const findLeafPaths = function (o, path) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    return entries .flatMap (function ([k, v]) {
      const paths = findLeafPaths (v, path || [[]])
      return paths .map (function(p) {
        return [k] .concat (p)
      })
    })
  }
  return path || [[]]
}

Now we can tweak this to take advantage of the modern spread syntax in place of concat:

const findLeafPaths = function (o, path) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    return entries .flatMap ( function ([k, v]) {
      const paths = findLeafPaths (v, path || [[]])
      return paths .map (function(p) {
        return [k, ...p]
      })
    })
  }
  return path || [[]]
}

Arrow functions will simplify things further. Here we replace the innermost function call with an arrow:

const findLeafPaths = function (o, path) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    return entries .flatMap ( function ([k, v]) {
      const paths = findLeafPaths (v, path || [[]])
      return paths .map (p => [k, ...p])
    })
  }
  return path || [[]]
}

We're repeating that path || [[]] expression in two places. We could use a default parameter to only have one:

const findLeafPaths = function (o, path = [[]]) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    return entries .flatMap ( function ([k, v]) {
      return findLeafPaths (v, path) .map (p => [k, ...p])
    })
  }
  return path
}

Now we replace the next function expression (supplied to entries.flatmap()) with an arrow:

const findLeafPaths = function (o, path = [[]]) {
  if (typeof o == 'object') {
    const entries = Object .entries (o)
    return entries .flatMap (
      ([k, v]) => findLeafPaths (v, path) .map (p => [k, ...p])
    )
  }
  return path
}

entries is a temporary variable that we use only once in the line after it's defined. We can remove it easily:

const findLeafPaths = function (o, path = [[]]) {
  if (typeof o == 'object') {
    return Object .entries (o) .flatMap (
      ([k, v]) => findLeafPaths (v, path) .map (p => [k, ...p])
    )
  }
  return path
}

From a functional perspective, working with expressions is preferable to working with statements. They are more susceptible to analysis and they don't depend on external ordering. Hence, I will choose a conditional expression ("ternary statement") to an if-else one. So I prefer this version:

const findLeafPaths = function (o, path = [[]]) {
  return typeof o == 'object'
    ? Object .entries (o) .flatMap (
        ([k, v]) => findLeafPaths (v, path) .map (p => [k, ...p])
      ) 
    : path
}

Finally, we can replace the outermost function expression with another arrow function to get the version in the answer above:

const findLeafPaths = (o, path = [[]]) => 
  typeof o == 'object'
    ? Object .entries (o) .flatMap (
        ([k, v]) => findLeafPaths (v, path) .map (p => [k, ...p])
      ) 
    : path

Obviously we could do the same sort of thing with path and makeSearcher as well.

Note that every step of this reduced the line or character count of the function. That is nice, but it is not at all the most important point. More relevant is that each version is arguably simpler than the one preceding it. This does not mean that it's more familiar, only that fewer ideas are being twined together. (Rich Hickey's Simple Made Easy talk does a great idea of explaining the difference between these often-confused notions.)

I work often with junior developers, and getting them through this transition is important to the growth of their skills. There were no difficult steps in there, but the end result is substantially simpler than the original. After some time, writing directly in this style can become second-nature.

这篇关于Typescript - 在多维对象中查找匹配的键和路径的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆