【问题标题】:Recurse through JSON to appending child key to parent通过 JSON 递归以将子键附加到父级
【发布时间】:2020-09-07 14:12:31
【问题描述】:

我希望实现的是使用递归或其他方法根据 json 输入动态创建特定的输出。下面只是一些json的例子和我希望完成的格式。

  "id": "0001",
  "type": "donut",
  "name": "Cake",
  "image": [
    {
      "url": "images/0001.jpg",
      "width": 200,
      "height": 200
    },
    {
      "url": "images/0002.jpg",
      "width": 300,
      "height": 300
    }
  ],
  "thumbnail": {
    "url": "images/thumbnails/0001.jpg",
    "width": 32,
    "height": 32
  }
}

使用以下格式构建一个键数组。

["id","type","name","image[0].url","image[0].width","image[0].height","image[1].url","image[1].width","image[1].height","thumbnail.url","thumbnail.width","thumbnail.height"]

在构建数组之后,我想遍历数组并构建一个如下所示的字符串。

[ "id" ; $id ; JSONString ];[ "type" ; $type ; JSONString ];[ "name" ; $name ; JSONString ];[ "image[0].url" ; $image.url[0] ; JSONString ];[ "image[0].width" ; $image.width[0] ; JSONString ];[ "image[0].height" ; $image.height[0] ; JSONString ];[ "image[1].url" ; $image.url[1] ; JSONString ]...

到目前为止,我已经能够在相当简单的 JSON 上完成这项工作,但在我尝试使用更复杂的结构时却没有得到任何结果。

JS 代码:已编辑

    var json = '{"id":"0001","type":"donut","name":"Cake","image":[{"url":"images/0001.jpg","width":200,"height":200},{"url":"images/0002.jpg","width":300,"height":300}],"thumbnail":{"url":"images/thumbnails/0001.jpg","width":32,"height":32}}';
    var obj = JSON.parse(json);


    // Recursion through the json 
    function getKeys(object) {
      return Object
        .entries(object)
        .reduce((r, [k, v]) =>
          r.concat(v && typeof v === 'object'
            ? getKeys(v).map(sub => [k].concat(sub))
            : k
          ),
          []
        );
    }

    function buildFM(object) {

      var objLength = object.length;
      var i = 1;
      var str = '';
      for (x of object) {
        var nodes = x.split(/\.(?=[^\.]+$)/);
        if (i == objLength) {
          str += '[ "' + x + '" ; $' + nodes[nodes.length - 1] + ' ; JSONString ]';
        } else {
          str += '[ "' + x + '" ; $' + nodes[nodes.length - 1] + ' ; JSONString ] ; ';
        }
        i++;
      }
      return str;
    }

    // Result from Recursion  
    result = getKeys(obj);
    console.log(result);


    // Setup Map of JSON for creating FM function
    var fmMap;
    fmMap = result.map(a => a.join('.'));
    console.log(fmMap);

    //  Build FM Elements
    var fmFX = '';
    var fmFX = buildFM(fmMap);
    console.log(fmFX);

使用下面的 JSON 这个方法可以正常工作。

var json = '{"fieldData":{"City":"New York","FirstName":"John","ID":"001","LastName":"Doe","State":"NY","Zip":"10005"}}'; 

在我尝试过的更复杂的示例中,出现以下错误。

错误:

Uncaught TypeError: a.join is not a function
    at jsonRecursion.html:216
    at Array.map (<anonymous>)
    at jsonParsing.html:216

如何处理更复杂的 json 以获取示例中的键数组?

【问题讨论】:

  • 你能举个更复杂的json失败的例子吗?
  • .join is not a function 表示它不是数组,这意味着您需要检查哪些映射值不是数组
  • 你能不能也把你的JS Code 放到一个sn-p 中?当我尝试将您的代码粘贴到 sn-p 中时,出现“无法获取未定义的长度”错误。您的示例可能需要修复
  • A. L. 编辑JS并放入sn-p。
  • @jimrice 我对pathToString 进行了编辑,我想你会喜欢的

标签: javascript arrays json recursion


【解决方案1】:

您可以使用通用的traverse 函数 -

function* traverse (value = {}, path = [])
{  if (Array.isArray(value))
     for (const [k, v] of value.entries())
       yield* traverse(v, [...path, k])
   else if (Object(value) === value)
     for (const [k, v] of Object.entries(value))
       yield* traverse(v, [...path, k])
   else
      yield { path, value }
}

然后您可以使用for..of 以线性方式遍历结果 -

const pathToString = (keys = []) =>
  keys.map(k => Number(k) === k ? `[${k}]` : k).join(".")

for (const { path, value } of traverse(obj))
  console.log([ pathToString(path), value, "JSONString" ])

展开下面的sn-p,在自己的浏览器中验证结果-

function* traverse (value = {}, path = [])
{  if (Array.isArray(value))
     for (const [k, v] of value.entries())
       yield* traverse(v, [...path, k])
   else if (Object(value) === value)
     for (const [k, v] of Object.entries(value))
       yield* traverse(v, [...path, k])
   else
      yield { path, value }
}

const pathToString = (keys = []) =>
  keys.map(k => Number(k) === k ? `[${k}]` : k).join(".")

const json = '{"id":"0001","type":"donut","name":"Cake","image":[{"url":"images/0001.jpg","width":200,"height":200},{"url":"images/0002.jpg","width":300,"height":300}],"thumbnail":{"url":"images/thumbnails/0001.jpg","width":32,"height":32}}';

const obj = JSON.parse(json);

for (const { path, value } of traverse(obj))
  console.log([ pathToString(path), value, "JSONString" ])

输出 -

[ "id", "0001", "JSONString" ]
[ "type", "donut", "JSONString" ]
[ "name", "Cake", "JSONString" ]
[ "image.[0].url", "images/0001.jpg", "JSONString" ]
[ "image.[0].width", 200, "JSONString" ]
[ "image.[0].height", 200, "JSONString" ]
[ "image.[1].url", "images/0002.jpg", "JSONString" ]
[ "image.[1].width", 300, "JSONString" ]
[ "image.[1].height", 300, "JSONString" ]
[ "thumbnail.url", "images/thumbnails/0001.jpg", "JSONString" ]
[ "thumbnail.width", 32, "JSONString" ]
[ "thumbnail.height", 32, "JSONString" ]

更新

上面,程序已经接近预期的输出,但我们必须更进一步才能满足问题中的精确要求 -

const pathToString = ([ init, ...keys ]) =>
  keys.reduce
    ( (whole = "", part) =>
        Number(part) === part
          ? `${whole}[${part}]` // <-- number
          : `${whole}.${part}`  // <-- string
    , init
    )

const template = (s = "") =>
 `[ "${s}" ; \$${s} ; JSONString ]`

for (const { path, value:_ } of traverse(obj))
  console.log(template(pathToString(path)))

function* traverse (value = {}, path = [])
{  if (Array.isArray(value))
     for (const [k, v] of value.entries())
       yield* traverse(v, [...path, k])
   else if (Object(value) === value)
     for (const [k, v] of Object.entries(value))
       yield* traverse(v, [...path, k])
   else
      yield { path, value }
}

const pathToString = ([ init, ...keys ]) =>
  keys.reduce
    ( (whole = "", part) =>
        Number(part) === part
          ? `${whole}[${part}]`
          : `${whole}.${part}`
    , init
    )
    
const template = (s = "") =>
  `[ "${s}" ; \$${s} ; JSONString ]`

const json = '{"id":"0001","type":"donut","name":"Cake","image":[{"url":"images/0001.jpg","width":200,"height":200},{"url":"images/0002.jpg","width":300,"height":300}],"thumbnail":{"url":"images/thumbnails/0001.jpg","width":32,"height":32}}';

const obj = JSON.parse(json);

for (const { path, value:_ } of traverse(obj))
  console.log(template(pathToString(path)))
[ "id" ; $id ; JSONString ]
[ "type" ; $type ; JSONString ]
[ "name" ; $name ; JSONString ]
[ "image[0].url" ; $image[0].url ; JSONString ]
[ "image[0].width" ; $image[0].width ; JSONString ]
[ "image[0].height" ; $image[0].height ; JSONString ]
[ "image[1].url" ; $image[1].url ; JSONString ]
[ "image[1].width" ; $image[1].width ; JSONString ]
[ "image[1].height" ; $image[1].height ; JSONString ]
[ "thumbnail.url" ; $thumbnail.url ; JSONString ]
[ "thumbnail.width" ; $thumbnail.width ; JSONString ]
[ "thumbnail.height" ; $thumbnail.height ; JSONString ]

现在,如果您希望将所有[ ... ] 结果通过; 连接到一个字符串中,我们可以使用Array.from 收集所有结果-

const result =
  Array.from
    ( traverse(obj)
    , ({ path, value:_ }) => template(pathToString(path))
    )
    .join(";")

console.log(result)
// [ "id" ; $id ; JSONString ];[ "type" ; $type ; JSONString ];[ "name" ; $name ; JSONString ];[ "image[0].url" ; $image[0].url ; JSONString ];[ "image[0].width" ; $image[0].width ; JSONString ];[ "image[0].height" ; $image[0].height ; JSONString ];[ "image[1].url" ; $image[1].url ; JSONString ];[ "image[1].width" ; $image[1].width ; JSONString ];[ "image[1].height" ; $image[1].height ; JSONString ];[ "thumbnail.url" ; $thumbnail.url ; JSONString ];[ "thumbnail.width" ; $thumbnail.width ; JSONString ];[ "thumbnail.height" ; $thumbnail.height ; JSONString ]

【讨论】:

  • 虽然我喜欢这种方法,但请参阅我的答案以了解多种变体。
【解决方案2】:

问题出在这行:

fmMap = result.map(a => a.join('.'));

并非一切都是数组,即:id、type、name

所以你需要在尝试.join它之前检查它是否是一个数组。

只需更改映射函数即可。

fmMap = result.map(a => Array.isArray(a) ? a.join('.') : a);

如果您只想为数字使用括号,您可以重新映射输入,根据数字正则表达式检查值,然后根据需要输出。

换行

const array_mapper = (v, i, arr) => {
  if (i > 0) {
    return v.match(/^\d+$/)
      ? "[" + v + "]"
      : "." + v;
  }
  return v
}

var json = '{"id":"0001","type":"donut","name":"Cake","image":[{"url":"images/0001.jpg","width":200,"height":200},{"url":"images/0002.jpg","width":300,"height":300}],"thumbnail":{"url":"images/thumbnails/0001.jpg","width":32,"height":32}}';
var obj = JSON.parse(json);


// Recursion through the json 
function getKeys(object) {
  return Object
    .entries(object)
    .reduce((r, [k, v]) =>
      r.concat(v && typeof v === 'object' ?
        getKeys(v).map(sub => [k].concat(sub)) :
        k
      ), []
    );
}

function buildFM(object) {

  var objLength = object.length;
  var i = 1;
  var str = '';
  for (x of object) {
    var nodes = x.split(/\.(?=[^\.]+$)/);
    if (i == objLength) {
      str += '[ "' + x + '" ; $' + nodes[nodes.length - 1] + ' ; JSONString ]';
    } else {
      str += '[ "' + x + '" ; $' + nodes[nodes.length - 1] + ' ; JSONString ] ; ';
    }
    i++;
  }
  return str;
}

// Result from Recursion  
result = getKeys(obj);
console.log(result);


// Setup Map of JSON for creating FM function
const array_mapper = (v, i, arr) => {
  if (i > 0) {
    return v.match(/^\d+$/)
      ? "[" + v + "]"
      : "." + v;
  }
  return v
}

var fmMap;
fmMap = result.map(a => 
  Array.isArray(a) 
    ? a.map(array_mapper).join("") 
    : a);
console.log(fmMap);

//  Build FM Elements
var fmFX = '';
var fmFX = buildFM(fmMap);
console.log(fmFX);

【讨论】:

  • 谢谢你。这是如此接近。但是对于索引号,它需要是 image[0].url 而不是 image.0.url。如果在这个函数中不可能,我想我可以创建一个函数来做到这一点。为了工作,格式非常具体。
  • @jimrice 已更新。我能想到的最简单的方法是通过添加你需要的东西来重新映射数组,然后加入一个空白字符串。
  • 我也推荐使用Thank you's traverse function,因为你的getKeys似乎很慢
【解决方案3】:

我很不清楚我们想要的最终格式是什么。示例输出中描述的内容似乎与可能或合乎逻辑的内容略有不同,并且代码中所做的内容与这两者都不匹配。在这里,我们编写了一些代码,允许我们将有问题的部分插入一些稍微通用的代码中。但是,它确实不是可重用的代码,因此最好在正确的解决方案明确时内联它。

我们从一个getPaths 函数开始,它列出了一个对象中所有叶子的路径,因此返回诸如['id']['type']['image', 1, 'url'] 之类的东西。这可以很容易地修改为@Thankyou 在另一个答案中所做的,以包括路径和该路径的值,只需将yield p 替换为yield {p, v: o}。但除非我们的第四个选项实际上是正确的,否则这里可能没有必要。代码如下:

const getPaths = (o) => {
  function * _getPaths(o, p) {
    if (Object(o) !== o || Object .keys (o) .length == 0) yield p 
    if (Object(o) === o)
      for (let k of Object .keys (o))
        yield * _getPaths (o[k], [...p, Number.isInteger (Number (k)) ? Number (k) : k])
  }
  return [..._getPaths(o, [])]
}

其中的内部函数有一个生成器,如果我们想简单地迭代结果而不是最终得到一个数组,我们可以使用它来代替。

接下来,我们编写代码,使用getPaths 为每个输出部分的第二个元素使用可自定义的格式生成输出。 (这是因为我不清楚实际要求。同样,我们可能会内联这个正确的版本。)

看起来像这样:

const format = (transformer) => (input) => 
  getPaths(input)
    .map(path => `[${[
      `"${combinePath (path)}"`,
      transformer (path, input),
      'JSONString'
    ].join(', ')}]`).join(';')

现在我们可以尝试不同的策略:

const t1 = (path, obj) =>
  `$${combinePath(path)}`

format (t1) (input) 生成的条目如下所示:["image[1].width", $image[1].width, JSONString]

但请求的输出末尾有括号。我不知道是否会对此作出澄清,但做出这种改变并不是非常困难。有了这个:

const t2 = (path, obj) =>
  `"$${
    path.filter(n => Number(n) !== n).join('.')
    + path.filter(n => Number(n) === n).map(n => `[${n}]`).join('')
  }"`

format (t2) (input) 生成的条目如下所示:["image[1].width", $image.width[1], JSONString]。 (注意第二部分中括号的位置发生了变化。)

这让我觉得这是一种奇怪的格式,尤其是在有嵌套数组的情况下,但如果这是真正想要的,它是可用的。现在来自 OP 的示例代码仍然做了一些不同的事情,所以如果我们只想要最后一个节点,我们可以这样写:

const t3 = (path, obj) => 
  `$${path.slice(-1)[0]}`

然后 format (t3) (input) 会产生类似 ["image[1].width", $width, JSONString] 的结果。同样,我不确定这有多大用处,但要求尚不清楚。

最后,如果我们想要第二个位置的原始对象的值,我们可以使用像下面这样的帮助器getPath(我通常称之为path,但在这里似乎被过度使用了)来编写像这样的版本:

const getPath = (ps) => (o) =>
  ps.reduce ((o, p) => o[p] || {}, o)
const t4 = (path, obj) =>
  `"${getPath (path) (obj)}"`

format (t4) (input) 会产生类似 ["image[1].width", "300", JSONString] 的结果。我们可以通过修改getPaths 来更简单地做到这一点,如上所述将值包含在结果中;然后我们可以跳过助手。这也将简化以下有些无关但显然有用的功能:

const flattenObj = (obj) =>
  Object .fromEntries (getPaths (obj) .map (p => [combinePath (p), getPath (p) (obj)]))

它使用这些相同的工具将我们的对象转换成这样的扁平形式:

{
  "id": "0001",
  "type": "donut",
  "name": "Cake",
  "image[0].url": "images/0001.jpg",
  "image[0].width": 200,
  "image[0].height": 200,
  "image[1].url": "images/0002.jpg",
  "image[1].width": 300,
  "image[1].height": 300,
  "thumbnail.url": "images/thumbnails/0001.jpg",
  "thumbnail.width": 32,
  "thumbnail.height": 32
}

我不清楚其中哪些真正满足您的需求(如果有的话)。但是一旦选择,它可以通过简单地删除 transformer 参数并将函数体代替 transformer(path, obj) 调用来适应格式函数。

你可以看到所有这些选项都在这个 sn-p 中运行:

const getPaths = (o) => {
  function * _getPaths(o, p) {
    if (Object(o) !== o || Object .keys (o) .length == 0) yield p 
    if (Object(o) === o)
      for (let k of Object .keys (o))
        yield * _getPaths (o[k], [...p, Number.isInteger (Number (k)) ? Number (k) : k])
  }
  return [..._getPaths(o, [])]
}

const combinePath = (path) => 
  path.reduce((r, n) => r + (Number(n) == (n) ? `[${Number(n)}]` : `.${n}`))

const format = (transformer) => (input) => 
  getPaths(input)
    .map(path => `[${[
      `"${combinePath (path)}"`,
      transformer (path, input),
      'JSONString'
    ].join(', ')}]`).join(';')


const t1 = (path, obj) =>
  `$${combinePath(path)}`

const t2 = (path, obj) =>
  `"$${
    path.filter(n => Number(n) !== n).join('.')
    + path.filter(n => Number(n) === n).map(n => `[${n}]`).join('')
  }"`

const t3 = (path, obj) => 
  `$${path.slice(-1)[0]}`

const getPath = (ps) => (o) =>
  ps.reduce ((o, p) => o[p] || {}, o)
const t4 = (path, obj) =>
  `"${getPath (path) (obj)}"`

const input = {"id": "0001", "type": "donut", "name": "Cake", "image": [{"url": "images/0001.jpg", "width": 200, "height": 200}, {"url": "images/0002.jpg", "width": 300, "height": 300}], "thumbnail": {"url": "images/thumbnails/0001.jpg", "width": 32, "height": 32}};

[t1, t2, t3, t4] .forEach ((t) => {
  console .log (format (t) (input))
  console .log ('---------------------------------')
})

const flattenObj = (obj) =>
  Object .fromEntries (getPaths (obj) .map (p => [combinePath (p), getPath (p) (obj)]))
 
console .log (flattenObj (input))
.as-console-wrapper {min-height: 100% !important; top: 0}

【讨论】:

    猜你喜欢
    • 2021-06-23
    • 2021-11-27
    • 2020-08-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-26
    • 2021-11-02
    • 1970-01-01
    相关资源
    最近更新 更多