如何用数组进行层级查询

前端开发 2026-07-12

我有一个这样的JavaScript数组:

let myArray = [
   { name: 'Alice', children: ['Bob', 'Bill'] },
   { name: 'Bob', children: 'Cindy' },
   { name: 'Bill', children: [] },
   { name: 'Cindy', children: ['David', 'Don'] },
   { name: 'David' },
   { name: 'Don' },
]

我想从中获取分层数据,也就是说,我想把数组展示成如下形式:

Alice
  Bob
    Cindy
      David
      Don
  Bill

或者

Alice
Alice -> Bob
Alice -> Bob -> Cindy
Alice -> Bob -> Cindy -> David
Alice -> Bob -> Cindy -> Don
Alice -> Bill

很多关系型数据库提供此类功能,例如Oracle的 [CONNECT BY] 子句,或者MongoDB的 [$graphLookup] 聚合操作符

我可以把这个数组插入到Oracle表或MongoDB集合中,然后运行上述查询之一,但我正在寻找一个不使用数据库的解决方案。

解决方案

与此同时找到了一个与我前面提到的数据库函数非常接近的解决方案。

function connectBy(array, startWith, connectWith, path = [], level = 0) {
   const filterFn = typeof startWith == 'function' ?
      startWith :
      function (x) {
         const children = startWith[startWith.length - 1][connectWith[1]];
         return Array.isArray(children) ? children.includes(x[connectWith[0]]) : children == x[connectWith[0]];
      };
   for (let item of array.filter(filterFn)) {
      if (path.includes(array.indexOf(item)))
         item.isCycle = true;    
      item.level = level;
      item.path = path.concat(array.indexOf(item));
      typeof startWith == 'function' ? startWith = [item] : startWith.push(item);
      if (!item.isCycle)
         startWith = connectBy(array, startWith, connectWith, item.path, level + 1);
   }
   return startWith;
}

参数 startWith 是一个用于获取顶层项的函数。connectWith 是一个由两个字符串组成的数组,用于定义用于连接的字段。

const myArray = [
   { name: 'Alice', children: ['Bob', 'Bill'] },
   { name: 'Bob', children: 'Cindy' },
   { name: 'Bill', children: [] },
   { name: 'Cindy', children: ['David', 'Don'] },
   { name: 'David',children: ['Bob'] },
   { name: 'Don' },
];
const arr = connectBy(
   myArray, 
   function (val) { return val.name == 'Alice'}, 
   ['name', 'children']
);

[
  { name: 'Alice', children: [ 'Bob', 'Bill' ], level: 0, path: [ 0 ] },
  {
    name: 'Bob',
    children: 'Cindy',
    level: 4,
    path: [ 0, 1, 3, 4, 1 ],
    isCycle: true
  },
  {
    name: 'Cindy',
    children: [ 'David', 'Don' ],
    level: 2,
    path: [ 0, 1, 3 ]
  },
  {
    name: 'David',
    children: [ 'Bob' ],
    level: 3,
    path: [ 0, 1, 3, 4 ]
  },
  {
    name: 'Bob',
    children: 'Cindy',
    level: 4,
    path: [ 0, 1, 3, 4, 1 ],
    isCycle: true
  },
  { name: 'Don', level: 3, path: [ 0, 1, 3, 5 ] },
  { name: 'Bill', children: [], level: 1, path: [ 0, 2 ] }
]

添加了 pathlevel 信息后,生成输出或进行其他转换就变得非常简单。

arr = connectBy(myArray, function (val) { return val.name == 'Alice'}, ['name', 'children']);

console.log(arr.map(x => ' '.repeat(2 * x.level) + x.name).join('\n'))

Alice
  Bob
    Cindy
      David
      Don
  Bill


console.log(arr.map(x => x.path.map(m => myArray[m].name).join(' -> ')).join('\n'))

Alice
Alice -> Bob
Alice -> Bob -> Cindy
Alice -> Bob -> Cindy -> David
Alice -> Bob -> Cindy -> Don
Alice -> Bill

它也同样适用于反向关系

myArray = [
   { name: 'Alice' },
   { name: 'Bob', parent: 'Alice' },
   { name: 'Bill', parent: 'Alice' },
   { name: 'Cindy', parent: 'Bob' },
   { name: 'David', parent: 'Cindy' },
   { name: 'Don', parent: 'Cindy' }
]
arr = connectBy(myArray, (val) => { return val.parent == null }, ['parent', 'name']);

或者在一对多关系的场景中:

myArray = [
   { name: 'Adam', children: ['Britney'] },
   { name: 'Alice', children: ['Britney'] },
   { name: 'Britney' }
]

arr = connectBy(myArray, (x) => { return x.children != null }, ['name', 'children']);
console.log(arr.map(x => ' '.repeat(2 * x.level) + x.name).join('\n'))

Adam
  Britney
Alice
  Britney

更新

这里给出一个不会修改输入数组的版本,path 包含来自输入的项:

function connectBy(array, startWith, connectWith, path = [], level = 0) {
   arr = [];
   for (let item of array)
      arr.push({ [connectWith[0]]: item[connectWith[0]], [connectWith[1]]: item[connectWith[1]] });
   const filterFn = typeof startWith == 'function' ?
      startWith :
      function (x) {
         const children = startWith[startWith.length - 1][connectWith[1]];
         return Array.isArray(children) ? children.includes(x[connectWith[0]]) : children == x[connectWith[0]];
      };
   for (let item of arr.filter(filterFn)) {
      if (path.includes(arr.indexOf(item)))
         item.isCycle = true;         
      item.level = level;
      item.path = path.concat(arr.indexOf(item));
      typeof startWith == 'function' ? startWith = [item] : startWith.push(item);
      if (!item.isCycle)
         startWith = connectBy(arr, startWith, connectWith, item.path, level + 1);
   }
   if (level == 0) {
      // Replace indices by original objects
      for (let item of startWith)
         item.path = item.path.filter( x => x >= 0).map( x => array[x] )
   }
   return startWith;
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章