JS数组转树(级联)

有时候,后端传递过来的数据并不是树姓结构:

[
    {"id":"12","parentId":"0","text":"Man","level":"1","children":null},
    {"id":"6","parentId":"12","text":"Boy","level":"2","children":null},
    {"id":"7","parentId":"12","text":"Other","level":"2","children":null},
    {"id":"9","parentId":"0","text":"Woman","level":"1","children":null},
    {"id":"11","parentId":"9","text":"Girl","level":"2","children":null}
]

这个数组的每一个子元素都有唯一的id,没有父节点时parentId为0,除0外的parentId必有id与其对应。

而我们期望的数组如下:

[
    {"id":"12","parentId":"0","text":"Man","level":"1","children":
            {"id":"6","parentId":"12","text":"Boy","level":"2","children":null},
            {"id":"7","parentId":"12","text":"Other","level":"2","children":null}
    {"id":"9","parentId":"0","text":"Woman","level":"1","children":
            {"id":"11","parentId":"9","text":"Girl","level":"2","children":null}
]

这里最直接的思路是先找到根节点(parentId为0),然后再将子节点放置进去。

array.push(obj)

在写代码之前,需要知道,当我们使用[].push添加一个对象时,实际上是将对象的指针加入到数组。数组和对象指向同一块内存区域,修改原对象的同时也会修改数组。

const arr = []
const obj = {a: 1, b: 2}
arr.push(obj)
obj['a'] = 111
console.log(arr) // [{a: 111, b: 2}]
console.log(obj) // {a: 111, b: 2}

那么,我们再处理数组的时候,就不必在意父子节点添加的顺序。只要修改原始对象,数组也会跟着修改。

简单版本

知道了方法,我们很容易就写出如下代码

function list_to_tree(list) {
    const roots = []
    for(let i = 0; i < list.length; i++){
      node = list[i]
      node.children = []
      if(node.parentId !== '0'){
        for(let j = 0; j < list.length; j++) {
          if(list[j].id === node.parentId) {
            list[j].children.push(node)
      }else {
        roots.push(node)
    return roots;

很容易看懂,先遍历原始数组,将没有父节点的放入数组,有父节点的再遍历数组中找到父节点,放置上去。

这个方法虽然简单,如果数据比较大(省份城市级联),处理起来就会慢,因为里面要两次循环遍历数组来寻找父节点,复杂度为O(n²)。

改进版

在上例的两个循环中,第一个循环很难改变,必然要遍历整个数组。第二个循环用于找父节点,我们可以考虑先生成一个存储id与所在位置的 对象表 ,加快寻址速度,避免每次找父节点都需要遍历数组。

function list_to_tree(list) {
    let map = {}, node, root = [], i;
    for (i = 0; i < list.length; i += 1) {
        map[list[i].id] = i; 
        // 要讲children初始化为数组
        list[i].children = [];
    console.log('map') // {6: 1, 7: 2, 9: 3, 11: 4, 12: 0}
    for (i = 0; i < list.length; i++) {
        node = list[i];
        if (node.parentId !== "0") {
            // 这里通过map来得到父节点的位置
            list[map[node.parentId]].children.push(node);
        } else {
            root.push(node);
    return root;

经过测试我们能够得到想要的结果。如果此时输出原始数据,会发现已经被更改,而且数据也远多于最开始。需要注意数据量比较大的时候,可能会有影响。

简单实例

下面来试验一下,我们有如下数据:

[
    {"id":1,"pid":0,"name":"上海市"},
    {"id":2,"pid":1,"name":"杨浦区"},
    {"id":3,"pid":1,"name":"静安区"},
    {"id":4,"pid":2,"name":"营口路"},
    {"id":5,"pid":3,"name":"北京西路"},
    {"id":6,"pid":2,"name":"长海路"},
    {"id":7,"pid":3,"name":"长寿路"},
    {"id":8,"pid":4,"name":"1号楼"},
    {"id":9,"pid":4,"name":"2号楼"}

用我们上面的方法稍加改动:

function list_to_tree(list) {
  let map = {}, node, root = [], i;
  for(i = 0; i < list.length; i++) {
    map[list[i].id] = i
    list[i].children = []
  for(i = 0; i < list.length; i++) {
    const node = list[i]
    if(list[i].pid !== 0) {
      list[map[node.pid]].children.push(node)
    }else {
      root.push(node)
  return root
}

处理后得到:

[
    {"id":1,"pid":0,"name":"上海市","children":
            {"id":2,"pid":1,"name":"杨浦区","children":
                    {"id":4,"pid":2,"name":"黄兴路","children":
                            {"id":8,"pid":4,"name":"1号楼","children":[]},
                            {"id":9,"pid":4,"name":"2号楼","children":[]}
                    {"id":6,"pid":2,"name":"西藏北路","children":[]}
            {"id":3,"pid":1,"name":"静安区","children":
                    {"id":5,"pid":3,"name":"延吉中路","children":[]},