问题描述
假设我们想要对公司中存在的层次结构进行建模。最基本的数据结构称为employee
。每个 employee
都是它自己的一个对象/树,并具有两个属性:level
是一个整数,reports
是一个包含 0 个或多个 employees
的对象/树。
我们可以说 employee
数据结构如下所示:
{
employee: {
level: Number,reports: Object //Contains 0 to N employees
}
}
小型企业可以表示其组织如下:
{
boss: {
level: 0,reports: {
employee0: {
level: 1,reports: {
anotherEmployee0: {
level: 2,reports: { /* ... */ },},anotherEmployee0: {
level: 2,employee1: {
level: 1,};
我们如何使用递归遍历这个结构?理想情况下,在没有语言函数抽象(例如 for ... in
、foreach
、map
)的情况下执行此操作。所以,“手动”。我很想看看这两种解决方案(有和没有所述抽象),以进行比较。
解决方法
for..in
是一种核心语言结构,从一开始就是 JavaScript 语言的一部分。这是提供的第一种检查对象属性的方法。
我在这里提供了两种解决方案。第一个将坚持您的数据结构,并使用 for..in
读取给定对象的属性并对 reports
值执行条件递归。它调用用户提供的回调函数,因此用户可以决定如何处理迭代值。
第二个切换到更好的数据结构,因为最好不要使用对象属性名称来表示动态内容。而是向您分配动态内容(“employee1”、“boss”等)的对象(可能是“名称”)添加一个属性。与老式的回调系统不同,它将函数定义为生成器。
解决方案 1(for..in
+ 回调):
这与 ES3 兼容。
function traverse(org,callback,parent) {
for (var name in org) {
var obj = org[name];
callback(name,obj.level,parent);
if (obj.reports) {
traverse(obj.reports,name);
}
}
}
// demo with original data structure
var org = {boss: {level: 0,reports: {employee0: {level: 1,reports: {anotherEmployee0: {level: 2,reports: { /* ... */ },},anotherEmployee1: {level: 2,employee1: {level: 1,};
traverse(org,visit,null);
// Our function that processes the iterated values:
function visit(name,level,parent) {
console.log(" ".slice(0,level) + name + " (level " + level + ") under " + (parent || "no one"));
}
方案二:(更好的数据结构+生成器)
这也使用解构、for..of
循环、模板文字、repeat
、let
、默认参数值、...
function * iterate(org,parent=null) {
for (let {name,reports} of org) {
yield [name,parent];
if (reports) {
yield * iterate(reports,name);
}
}
}
let org2 = [{
name: "boss",level: 0,reports: [{
name: "employee0",level: 1,reports: [{
name: "anotherEmployee0",level: 2,reports: [/* ... */]
},{
name: "anotherEmployee1",reports: [/* ... */]
}]
},{
name: "employee1",reports: [/* ... */]
}]
}];
for (let [name,parent] of iterate(org2)) {
console.log(`${" ".repeat(level)}${name} (level ${level}) under ${parent || "no one"}`);
}
Trincot 的出色回答使用了生成器“而不是老式的回调系统”。
以下是使用 trincot 对输入数据的重构来演示旧式系统如何工作的示例:
const traverseOrg = (fn) => (org,boss = null) =>
org .forEach (emp => {
fn (emp,boss)
traverseOrg (fn) (emp.reports || [],emp)
})
let org2 = [{name: "boss",reports: [{name: "employee0",reports: [{name: "anotherEmployee0",reports: [/* ... */]},{name: "anotherEmployee1",reports: [/* ... */]}]},{name: "employee1",reports: [/* ... */]}]}];
traverseOrg (
({level,name},boss) => console .log (
`${' ' .repeat (level)}${name} (level ${level}) reports to ${boss ? boss.name : "no one"}`
)
) (org2)
我们的 traverseOrg
函数接受一个回调函数并生成一个函数,该函数接受一组员工及其嵌套报告,并在深度优先遍历中调用每个员工的回调,传递员工和父员工,如果不存在则为 null。
我们可以将其用于与 trincot 演示的相同类型的输出,只需记录员工姓名、级别和老板姓名。
请注意,我们在这里使用 forEach
。我们可以使用多种类型的循环构造,但由于我们将其视为简单的遍历而不是转换,因此 .map
或 .reduce
等数组函数在此处不合适。
我们可以在此之上分层一个扁平化函数,如下所示:
const flattenOrg = (org) => {
const res = []
traverseOrg (({name,reports = []}) => res .push (
{name,directReports: reports .map (({name}) => name)}
)) (org)
return res
}
flattenOrg (org2)
将返回此结构:
[
{name: "boss",directReports: ["employee0","employee1"]},{name: "employee0",directReports: ["anotherEmployee0","anotherEmployee1"]},{name: "anotherEmployee0",directReports: []},directReports: []}
]
但这没有太多理由。这是丑陋的代码,我们最好直接编写它:
const flattenOrg = (org) =>
org .flatMap (({name,reports = []}) =>
[{name,directReports: reports .map (({name}) => name)},... flattenOrg (reports)]
)