# 1、入门级方法

function transformToTree(data) {
  // 创建一个空对象用于存储键值对形式的节点映射
  const nodeMap = {};
  
  // 遍历原始数据,构建节点映射表并初始化节点的 children 属性
  data.forEach(item => {
    const node = { id: item.id, name: item.name, children: [] };
    nodeMap[item.id] = node;
    
    if (item.pid !== 0) {
      // 如果当前节点不是根节点,则将其添加到其父节点的 children 数组中
      if (nodeMap[item.pid]) {
        nodeMap[item.pid].children.push(node);
      }
    }
  });

  // 从映射表中提取所有 pid 为 0 的根节点
  return Object.values(nodeMap).filter(node => node.pid === 0);
}

// 使用给定的数据进行转换
const menuData = [
  { id:1 , name:"关于我们" , pid:0 },
  { id:2 , name:"公司介绍" , pid:1 },
  { id:3 , name:"公司动态" , pid:1 },
  { id:4 , name:"公司团建" , pid:1 },
  { id:5 , name:"公司简介" , pid:2 },
  { id:6 , name:"企业资质" , pid:2 },
  { id:7 , name:"企业文化" , pid:2 },
  { id:8 , name:"发展历程" , pid:2 },
];

const treeData = transformToTree(menuData);

console.log(treeData);

说明:

上述代码首先创建了一个节点映射表,然后遍历原始数据,构建了每个节点以及它们之间的父子关系。最后,从映射表中提取出所有父节点ID为0的根节点作为最终的树形结构。运行此代码后,您将得到按照层级关系组织的树形菜单数据。

# 2、增加一个可选的参数 rootPid 来指定哪个 pid 应被视为根节点

为了增强该方法的灵活性,我们可以增加一个可选的参数 rootPid 来指定哪个 pid 应被视为根节点。默认情况下,如果未传递 rootPid 参数,则视为根节点的 pid 为 0

function transformToTree(data, rootPid = 0) {
  const nodeMap = {};

  data.forEach(item => {
    const node = { id: item.id, name: item.name, children: [] };
    nodeMap[item.id] = node;

    if (item.pid !== rootPid) {
      if (nodeMap[item.pid]) {
        nodeMap[item.pid].children.push(node);
      }
    }
  });

  return Object.values(nodeMap).filter(node => node.pid === rootPid);
}

// 示例数据
const menuData = [
  { id:1 , name:"关于我们" , pid:0 },
  { id:2 , name:"公司介绍" , pid:1 },
  { id:3 , name:"公司动态" , pid:1 },
  { id:4 , name:"公司团建" , pid:1 },
  { id:5 , name:"公司简介" , pid:2 },
  { id:6 , name:"企业资质" , pid:2 },
  { id:7 , name:"企业文化" , pid:2 },
  { id:8 , name:"发展历程" , pid:2 },
];

// 默认情况,rootPid 为 0
const defaultTreeData = transformToTree(menuData);

// 自定义 rootPid 为其他值时的示例
const customTreeData = transformToTree(menuData, 1); // 此处以 pid 为 1 的节点作为树的根节点

console.log(defaultTreeData);
console.log(customTreeData);

说明:

transformToTree 方法接受一个可选的 rootPid 参数,可以根据需要灵活地指定树形结构的根节点。

# 3、ES6 的解构赋值特性对方法进行优化

为了让代码更加简洁和自动化,您可以利用 ES6 的解构赋值特性来根据 menuData 数据项的属性自动抽取对应的值来创建 node 对象。下面是优化后的代码:

function transformToTree(data, rootPid = 0) {
  const nodeMap = {};

  data.forEach(({ id, name, pid }) => {
    const node = { id, name, children: [] };
    nodeMap[id] = node;

    if (pid !== rootPid) {
      if (nodeMap[pid]) {
        nodeMap[pid].children.push(node);
      }
    }
  });

  return Object.values(nodeMap).filter(node => node.pid === rootPid);
}

// 示例数据
const menuData = [
  { id:1 , name:"关于我们" , pid:0 },
  { id:2 , name:"公司介绍" , pid:1 },
  { id:3 , name:"公司动态" , pid:1 },
  { id:4 , name:"公司团建" , pid:1 },
  { id:5 , name:"公司简介" , pid:2 },
  { id:6 , name:"企业资质" , pid:2 },
  { id:7 , name:"企业文化" , pid:2 },
  { id:8 , name:"发展历程" , pid:2 },
];

// 默认情况,rootPid 为 0
const defaultTreeData = transformToTree(menuData);

// 自定义 rootPid 为其他值时的示例
const customTreeData = transformToTree(menuData, 1);

console.log(defaultTreeData);
console.log(customTreeData);

说明:

这样,在 forEach 循环内部直接通过解构赋值的方式抽取了每个元素的 id、name 和 pid 属性,使得代码更具通用性和易于维护。如果将来数据结构有所调整,只需更改 menuData 中的对象结构即可,无需改动 transformToTree 函数内部逻辑。

# 4、适配提供的数据含有其他属性的情况

//如:原始数据如下
[
    { id:1 , name:"关于我们" , pid:0 },
    { id:2 , name:"公司介绍" , pid:1 , enname:"about us"},
    { id:3 , name:"公司动态" , pid:1 },
    { id:4 , name:"公司团建" , pid:1 },
    { id:5 , name:"公司简介" , pid:2 },
    { id:6 , name:"企业资质" , pid:2 },
    { id:7 , name:"企业文化" , pid:2 },
    { id:8 , name:"发展历程" , pid:2 },
]

// 针对新增的属性字段,例如这里的 enname,我们可以保持节点对象的构造方式不变,仅保留必要的属性和新的自定义属性。这样当数据中有其他属性时,仍然能够正确构建树形结构,同时保留所有原数据中的属性。以下是更新后的代码:
function transformToTree(data, rootPid = 0) {
  const nodeMap = {};

  data.forEach(({ id, name, pid, ...otherProps }) => {
    const node = { id, name, pid, children: [], ...otherProps };
    nodeMap[id] = node;

    if (pid !== rootPid && nodeMap[pid]) {
      nodeMap[pid].children.push(node);
    }
  });

  return Object.values(nodeMap).filter(node => node.pid === rootPid);
}

// 示例数据
const menuData = [
  { id:1 , name:"关于我们" , pid:0 },
  { id:2 , name:"公司介绍" , pid:1 , enname:"about us"},
  { id:3 , name:"公司动态" , pid:1 },
  { id:4 , name:"公司团建" , pid:1 },
  { id:5 , name:"公司简介" , pid:2 },
  { id:6 , name:"企业资质" , pid:2 },
  { id:7 , name:"企业文化" , pid:2 },
  { id:8 , name:"发展历程" , pid:2 },
];

// 默认情况,rootPid 为 0
const treeData = transformToTree(menuData);

console.log(treeData);

说明:

在此代码中,我们使用了扩展运算符 (...) 将除 id、name、pid 之外的所有其他属性合并到新创建的 node 对象中,这样即使后续有更多额外属性也能被正确包含在内。

# 5、在4的返回的树形结构结果上,自动加上一个层级属性level

为了在返回的树形结构结果上自动加上一个表示层级的 level 属性,我们需要遍历时跟踪每个节点所在的层级,并将其添加到相应的节点对象中。以下是在原有基础上增加层级属性的方法:

function transformToTree(data, rootPid = 0, level = 0) {
  const nodeMap = {};

  data.forEach(({ id, name, pid, ...otherProps }) => {
    const node = { id, name, pid, level, children: [], ...otherProps };
    nodeMap[id] = node;

    if (pid !== rootPid && nodeMap[pid]) {
      nodeMap[pid].children.push(node);
      // 更新子节点的层级为父节点层级 + 1
      node.level = nodeMap[pid].level + 1;
    } else if (pid === rootPid) {
      // 根节点层级为 0 或者自定义初始层级
      node.level = level;
    }
  });

  return Object.values(nodeMap).filter(node => node.pid === rootPid);
}

// 示例数据
const menuData = [
  { id:1 , name:"关于我们" , pid:0 },
  { id:2 , name:"公司介绍" , pid:1 , enname:"about us"},
  { id:3 , name:"公司动态" , pid:1 },
  { id:4 , name:"公司团建" , pid:1 },
  { id:5 , name:"公司简介" , pid:2 },
  { id:6 , name:"企业资质" , pid:2 },
  { id:7 , name:"企业文化" , pid:2 },
  { id:8 , name:"发展历程" , pid:2 },
];

// 默认情况,rootPid 为 0,层级为 0
const treeData = transformToTree(menuData);

console.log(treeData);

说明:

在这个实现中,我们在遍历数据构建树的同时,计算并设置了每个节点的 level 属性,确保了每个节点的层级信息准确反映在树形结构中。

# 6、演变最终方法:兼容性和扩展性更好的方法(对更为原始的数据进行处理,只包含节点id,和父节点pid)

//如:原始数据如下
[
    { id:1 , pid:0 },
    { id:2 , pid:1,enname:"about us" },
    { id:3 , pid:1 },
    { id:4 , pid:1 },
    { id:5 , pid:2 },
    { id:6 , pid:2 },
    { id:7 , pid:2 },
    { id:8 , pid:2 },
]  
// 考虑到提供的最新数据结构中移除了 name 字段,假设您希望在转换过程中依然保留原有的功能,我们可以设定一个默认的 name 值(例如“未命名”)来填充缺失的 name,然后再进行树形结构的构建。以下是相应的方法改写:
function transformToTree(data, rootPid = 0, level = 0, defaultName = "未命名") {
  const nodeMap = {};

  data.forEach(({ id, pid, enname, ...otherProps }) => {
    let name = enname || defaultName;
    const node = { id, name, pid, level, children: [], ...otherProps };
    nodeMap[id] = node;

    if (pid !== rootPid && nodeMap[pid]) {
      nodeMap[pid].children.push(node);
      node.level = nodeMap[pid].level + 1;
    } else if (pid === rootPid) {
      node.level = level;
    }
  });

  return Object.values(nodeMap).filter(node => node.pid === rootPid);
}

// 示例数据
const menuData = [
  { id:1 , pid:0 },
  { id:2 , pid:1, enname:"about us" },
  { id:3 , pid:1 },
  { id:4 , pid:1 },
  { id:5 , pid:2 },
  { id:6 , pid:2 },
  { id:7 , pid:2 },
  { id:8 , pid:2 },
];

// 默认情况,rootPid 为 0,层级为 0,默认名称为 "未命名"
const treeData = transformToTree(menuData);

console.log(treeData);

说明:

在这段代码中,我们检查了每条记录是否存在 enname 字段,如果有则将其用作 name,否则使用默认名称 "未命名"。同时,之前添加层级属性的功能也得到了保留。

# 7、结果展示

//测试
//测试树形结构
const menuData = [
    { id: 1, name: "关于我们", pid: 0 },
    { id: 2, name: "公司介绍", pid: 1 , enname:"about us"},
    { id: 3, name: "公司动态", pid: 1 },
    { id: 4, name: "公司团建", pid: 1 },
    { id: 5, name: "公司简介", pid: 2 },
    { id: 6, name: "企业资质", pid: 2 },
    { id: 7, name: "企业文化", pid: 2 },
    { id: 8, name: "发展历程", pid: 2 },
];
// 默认情况,rootPid 为 0,层级为 0,默认名称为 "未命名"
const res = app.transformToTree(menuData);
console.log('测试树形结构', JSON.stringify(res));

//结果展示:
[
  {
    "id": 1,
    "name": "关于我们",
    "pid": 0,
    "level": 0,
    "children": [
      {
        "id": 2,
        "name": "公司介绍",
        "pid": 1,
        "level": 1,
        "enname":"about us",
        "children": [
          {
          "id": 5,
          "name": "公司简介",
          "pid": 2,
          "level": 2,
          "children": []
          }, 
          {
            "id": 6,
            "name": "企业资质",
            "pid": 2,
            "level": 2,
            "children": []
          }, 
          {
            "id": 7,
            "name": "企业文化",
            "pid": 2,
            "level": 2,
            "children": []
          }, 
          {
            "id": 8,
            "name": "发展历程",
            "pid": 2,
            "level": 2,
            "children": []
          }
        ]
      }, 
    {
      "id": 3,
      "name": "公司动态",
      "pid": 1,
      "level": 1,
      "children": []
    }, 
    {
      "id": 4,
      "name": "公司团建",
      "pid": 1,
      "level": 1,
      "children": []
    }]
  }
]
更新时间: 2024年4月2日星期二上午9点47分