首页 > 解决方案 > 如何将 ramda 食谱中的 flattenObj 函数转换为迭代函数

问题描述

我正在处理一个测试环境 nodejs/sequelize/mocha/chai。我发现这个flattenObj在测试对象时非常有用,例如由 sequelize 生成。
它使这些结构易于 chai 消化,结果变得更加简洁
太糟糕了,它是以递归方式实现的:( 。
尤其是在 Javascript 中,这意味着厄运,因为总是潜伏着调用堆栈限制。
像将递归函数包装在 setTimeout 中的黑客似乎对我不起作用并且有点难看。
我目前正在尝试以迭代的方式重写它,但至少对我来说,这是一个脑筋急转弯。
处理 ramda 函数中的 while 循环感觉不对。
有没有一种方法可以在不破坏 ramda 约定的情况下以调用堆栈友好的方式做到这一点?

const go = obj_ => chain(([k, v]) => {
  if (type(v) === 'Object' || type(v) === 'Array') {
    return pipe( 
        tap(console.log),            
        map(([k_, v_]) => [`${k}.${k_}`, v_]) 
     )(go(v))
  } else {
    return [[k, v]]
  }
}, toPairs(obj_))

const flattenObj = obj => {
  return fromPairs(go(obj))
}

flattenObj({a:1, b:{c:3}, d:{e:{f:6}, g:[{h:8, i:9}, 0]}})

{
  "a": 1,
  "b.c": 3,
  "d.e.f": 6,
  "d.g.0.h": 8,
  "d.g.0.i": 9,
  "d.g.1": 0
}

这可以按预期工作,但是当对象变得太复杂时,由于递归 go 函数,它会分解导致调用堆栈超出错误。
如果它也适用于更复杂的结构,这将非常有用。

标签: objectrecursionflattenramda.js

解决方案


我认为它以递归方式实现并不是一件坏事。这是处理递归数据结构(如 JS 对象)的最佳方式。

但是,如果您想管理自己的堆栈,您始终可以将递归解决方案转换为迭代解决方案。这是一个相当丑陋的方法,但它似乎适用于那个简单的测试用例:

const flattenObj = (obj) => {
  const results = [];
  const steps = Object.entries(obj)
  while (steps.length) {
    const [key, val] = steps.splice(0, 1)[0]
    if (typeof val == 'object') {
      Array.prototype.push.apply(steps, Object.entries(val).map(
        ([k, v]) => [key + '.' + k, v]
      ))
    } else {
      results.push([key, val])
    }
  }
  return results.reduce((a, [k, v]) => ({...a, [k]: v}), {})
}


const foo = {a:1, b:{c:3}, d:{e:{f:6}, g:[{h:8, i:9}, 0]}}

console.log(flattenObj(foo))

这不适用于循环结构,但大概食谱版本也不会。

我最初是使用一些 Ramda 函数(toPairs代替Object.entriesis(Object, val)代替typeof val == 'object'return fromPairs(results)代替return results.reduce(...))编写的。但是随着所有突变的发生(splicepush),它感觉是一个非常非 Ramda-ish 的解决方案,我删除了它们。(Ramda 函数,你明白的,不想与 gauche mutability 相关联!)

我不知道这是否能解决你的问题。尽管我可以在测试中看到该实用程序,但我只使用flattenObj了几次。但令我震惊的是,如果这会导致递归问题,那么循环数据结构比实际深度更可能是一个问题。但是我当然不知道你的数据,所以谁知道呢?


推荐阅读