@Scott 帮您解释了您提出的数据形状和程序存在的问题。他是正确的,递归不是特别适合这个问题。他的回答确实激发了一个想法,我将在下面分享。
这里有collapse,它允许您使用named 键的可变长度序列折叠任意形状的对象 -
- 如果
name 为空,则已达到基本情况。合并中间结果 r 和输入 t
- (感应)
name 不为空。折叠输入 t 并以较小的子问题重复出现
const collapse = ([ name, ...more ], t = {}, r = {}) =>
name === undefined
? [ { ...r, ...t } ] // 1
: Object // 2
.entries(t)
.flatMap
( ([ k, v ]) =>
collapse(more, v, { ...r, [name]: k }) // <- recursion
)
const result =
collapse(["month", "day", "event", "_"], shows)
console.log(JSON.stringify(result, null, 2))
[ { "month": "1", "day": "29", "event": "0", "_": "0", "id": "0001", "title": "title a" }
, { "month": "1", "day": "29", "event": "1", "_": "1", "id": "0002", "title": "title b" }
, { "month": "1", "day": "30", "event": "0", "_": "0", "id": "0003", "title": "title c" }
, { "month": "1", "day": "30", "event": "1", "_": "1", "id": "0004", "title": "title a" }
, { "month": "6", "day": "29", "event": "0", "_": "0", "id": "0005", "title": "title d" }
, { "month": "6", "day": "29", "event": "1", "_": "1", "id": "0006", "title": "title b" }
, { "month": "6", "day": "30", "event": "0", "_": "0", "id": "0007", "title": "title a" }
, { "month": "6", "day": "30", "event": "1", "_": "1", "id": "0008", "title": "title c" }
]
感谢collapse,现在写getSimilarShows 更容易了-
const getSimilarShows = (shows = [], query = "") =>
collapse(["month", "day", "event", "_"], shows) // <-
.filter(v => v.title === query)
const result =
getSimilarShows(shows, "title b")
console.log(JSON.stringify(result, null, 2))
[ { "month": "1", "day": "29", "event": "1", "_": "1", "id": "0002", "title": "title b" }
, { "month": "6", "day": "29", "event": "1", "_": "1", "id": "0006", "title": "title b" }
]
注意
NB collapse 有点鲁莽,并不能保护您免于尝试过度折叠对象。例如,如果您提供四 (4) 个命名键,但对象仅嵌套两 (2) 层深,则将返回空结果 []。这可能是意料之外的,在这种情况下最好抛出运行时错误。
一个明显的改进是能够使用已知名称“跳过”关卡,例如上面的"_" -
const collapse = ([ name, ...more ], t = {}, r = {}) =>
name === undefined
? [ { ...r, ...t } ]
: Object
.entries(t)
.flatMap
( ([ k, v ]) =>
name === "_" // <- skip this level?
? collapse(more, v, r) // <- new behaviour
: collapse(more, v, { ...r, [name]: k }) // <- original
)
const result =
collapse(["month", "day", "event", "_"], shows)
console.log(JSON.stringify(result, null, 2))
有了这个更新,"_" 键不会出现在下面的输出中 -
[ { "month": "1", "day": "29", "event": "0", "id": "0001", "title": "title a" }
, { "month": "1", "day": "29", "event": "1", "id": "0002", "title": "title b" }
, { "month": "1", "day": "30", "event": "0", "id": "0003", "title": "title c" }
, { "month": "1", "day": "30", "event": "1", "id": "0004", "title": "title a" }
, { "month": "6", "day": "29", "event": "0", "id": "0005", "title": "title d" }
, { "month": "6", "day": "29", "event": "1", "id": "0006", "title": "title b" }
, { "month": "6", "day": "30", "event": "0", "id": "0007", "title": "title a" }
, { "month": "6", "day": "30", "event": "1", "id": "0008", "title": "title c" }
]
@Scott 提供了一个很好的建议,即使用原生 Symbol 或基于字符串的键。关注下方collapse.skip -
const collapse = (...) =>
name === undefined
? //...
: Object
.entries(t)
.flatMap
( ([ k, v ]) =>
name === collapse.skip // <- known symbol
? //...
: //...
)
collapse.skip = // <- define symbol
Symbol("skip")
现在我们使用collapse.skip,而不是赋予"_" 特殊行为。为了使示例保持一致,我们只跳过了一层嵌套,但我们可以有效地跳过任意数量的嵌套 -
const result =
collapse(["month", "day", "event", collapse.skip], shows) // <-
console.log(JSON.stringify(result, null, 2))
// ...
替代实施
我花了一些时间思考collapse,我想知道修改呼叫站点如何增加它的实用性 -
function collapse (t = {}, ...f)
{ function loop (t, c, r)
{ if (c >= f.length)
return [ { ...r, ...t } ]
else
return Object
.entries(t)
.flatMap(([ k, v ]) => loop(v, c + 1, { ...r, ...f[c](k) }))
}
return loop(t, 0, {})
}
const shows =
{1:{29:[{0:{id:'0001',title:'title a'}},{1:{id:'0002',title:'title b'}}],30:[{0:{id:'0003',title:'title c'}},{1:{id:'0004',title:'title a'}}]},6:{29:[{0:{id:'0005',title:'title d'}},{1:{id:'0006',title:'title b'}}],30:[{0:{id:'0007',title:'title a'}},{1:{id:'0008',title:'title c'}}]}}
const result =
collapse
( shows
, v => ({ month: v })
, v => ({ day: v })
, v => ({ event: v })
, v => ({}) // <- "skip"
)
console.log(JSON.stringify(result, null, 2))
类列表数组解构
虽然考虑数组索引很痛苦,但我同意下面@Scott 的评论。但是使用剩余参数进行解构可以创建很多中间值。这是一种技术,likeList,我一直在玩弄它似乎具有良好的人体工程学和内存占用 -
const likeList = (t = [], c = 0) =>
({ [Symbol.iterator]: function* () { yield t[c]; yield likeList(t, c + 1) } })
function collapse (t = {}, ...f)
{ function loop (t, [f, fs], r) // <- destructure without rest
{ if (f === undefined) // <- base case: no f
return [ { ...r, ...t } ]
else
return Object
.entries(t)
.flatMap(([ k, v ]) => loop(v, fs, { ...r, ...f(k) })) // <- f
}
return loop(t, likeList(f), {}) // <- likeList
}
或者可能 -
const likeList = (t = [], c = 0) =>
({ [Symbol.iterator]: _ => [ t[c], likeList(t, c + 1) ].values() })
保持性能
我是功能风格的大力倡导者,因为它释放了我们以完全不同的方式思考问题的能力。 JavaScript 对函数式程序员非常友好,但也有一些注意事项。以特定方式使用某些功能会减慢我们的程序速度,有时会让我们认为功能风格本身就是罪魁祸首。
我个人的爱好是探索新的方法来表达不会对性能造成很大影响的函数式程序。以上likeList 提供了解决方案。下面我们将在比较四 (4) 个复制数组的程序时对其进行测试。除了遍历输入数组的方式之外,每个程序都是相同的。
这里是用 rest 参数解构复制。 JavaScript 的原生解构语法支持的优雅形式。然而,它的成本很高,我们稍后会看到 -
const copyDestructure = (arr) =>
loop
( ( [ x, ...xs ] = arr // <- rest argument
, r = []
) =>
x === undefined
? r
: recur(xs, push(r, x))
)
这里是使用数字索引的副本。这将解构语法换成廉价索引。但是现在程序员有负担考虑数组边界、中间状态和非一错误-
const copyIndex = (arr) =>
loop
( ( i = 0 // <- index
, r = []
) =>
i >= arr.length // <- off-by-one?
? r
: recur(i + 1, push(r, arr[i])) // <- increment i
)
这是使用likeList 的副本。这使用解构语法,但没有昂贵的 rest 参数。我们消除了使用索引的所有负面担忧,但我们能保持良好的性能吗? -
const copyLikeList = (arr) =>
loop
( ( [ x, xs ] = likeList(arr) // <- likeList
, r = []
) =>
x === undefined
? r
: recur(xs, push(r, x)) // <- plainly use x and xs
)
并通过listList 复制,使用替代实现-
const copyLikeList2 = (arr) =>
loop
( ( [ x, xs ] = likeList2(arr) // <- implementation 2
, r = []
) =>
x === undefined
? r
: recur(xs, push(r, x)) // <- same
)
以毫秒为单位的运行时间,越低越好 -
Array size 100 1,000 10,000 100,000
-----------------------------------------------------
copyDestructure 3.30 19.23 482.3 97,233.5
copyIndex 0.47 5.92 20.9 165.1 <-
copyLikeList 1.18 9.31 55.6 479.2
copyLikeList2 0.79 7.90 33.6 172.4 <-
以 KB 为单位使用的内存,越低越好 -
Array size 1,000 100,000
-----------------------------------------------------
copyDestructure 613.43 38,790.34
copyIndex 247.60 4,133.72 <-
copyLikeList 960.44 26,885.91
copyLikeList2 233.63 2,941.98 <-
实施-
// Arr.js
const likeList = (t = [], c = 0) =>
({ [Symbol.iterator]: function* () { yield t[c]; yield likeList(t, c + 1) } })
const likeList2 = (t = [], c = 0) =>
({ [Symbol.iterator]: _ => [ t[c], likeList2(t, c + 1) ].values() })
const push = (t = [], x) =>
( t.push(x)
, t
)
const range = (start = 0, end = 0) =>
Array.from(Array(end - start), (_, n) => n + start)
export { likeList, likeList2, push, range }
// TailRec.js
function loop (f, ...init)
{ let r = f(...init)
while (r && r.recur === recur)
r = f(...r)
return r
}
const recur = (...v) =>
({ recur, [Symbol.iterator]: _ => v.values() })
export { loop, recur }
备注
copyLikeList2 上面使用likeList 的第二个实现真的很重要。性能特征与使用索引相当,即使在大输入上也是如此。 copyDestructure 即使在小至 1,000 个元素的数组上也明显变慢。