Skip to content

扁平化

JavaScript 中树结构转换是常见需求,主要包括「数组转树」和「树转数组(扁平化)」两种场景。以下是详细示例,包含完整实现和应用场景说明。

1. 数组转树结构(核心场景)

将扁平数组(含 idparentId)转换为嵌套树结构,适用于后端返回扁平数据,前端需要层级展示的场景。

(1)基础数据定义

假设有如下扁平数组(通常来自后端接口):

javascript
// 扁平数组(含 id 和 parentId,parentId 为 null 表示根节点)
const flatData = [
  { id: 1, name: "技术部", parentId: null },
  { id: 2, name: "产品部", parentId: null },
  { id: 11, name: "前端组", parentId: 1 },
  { id: 12, name: "后端组", parentId: 1 },
  { id: 111, name: "张三", parentId: 11 },
  { id: 112, name: "李四", parentId: 11 },
  { id: 121, name: "王五", parentId: 12 },
  { id: 21, name: "赵六", parentId: 2 },
];

目标:转换为嵌套树结构(每个节点包含 children 数组)。

(2)实现方法(高效版)

使用「哈希表映射」+「一次遍历」实现,时间复杂度 O(n):

javascript
/**
 * 扁平数组转树结构
 * @param {Array} flatArray - 扁平数组(每个元素需包含 id 和 parentId)
 * @param {string|number|null} rootParentId - 根节点的 parentId(默认 null)
 * @returns {Array} 转换后的树结构
 */
function arrayToTree(flatArray, rootParentId = null) {
  // 1. 创建哈希表(id 到节点的映射),方便快速查找父节点
  const nodeMap = new Map();
  // 2. 初始化所有节点(添加空 children)
  flatArray.forEach((item) => {
    nodeMap.set(item.id, { ...item, children: [] });
  });

  // 3. 构建树(关联父子节点)
  const tree = [];
  flatArray.forEach((item) => {
    const currentNode = nodeMap.get(item.id);
    const parentId = item.parentId;

    if (parentId === rootParentId) {
      // 根节点:直接加入树
      tree.push(currentNode);
    } else {
      // 子节点:添加到父节点的 children 中
      const parentNode = nodeMap.get(parentId);
      if (parentNode) {
        // 防止 parentId 无效的情况
        parentNode.children.push(currentNode);
      }
    }
  });

  return tree;
}

(3)使用示例

javascript
// 转换为树结构
const tree = arrayToTree(flatData);
console.log("数组转树结果:", tree);

输出结果(嵌套树结构):

javascript
[
  {
    id: 1,
    name: "技术部",
    parentId: null,
    children: [
      {
        id: 11,
        name: "前端组",
        parentId: 1,
        children: [
          { id: 111, name: "张三", parentId: 11, children: [] },
          { id: 112, name: "李四", parentId: 11, children: [] },
        ],
      },
      {
        id: 12,
        name: "后端组",
        parentId: 1,
        children: [{ id: 121, name: "王五", parentId: 12, children: [] }],
      },
    ],
  },
  {
    id: 2,
    name: "产品部",
    parentId: null,
    children: [{ id: 21, name: "赵六", parentId: 2, children: [] }],
  },
];

2. 树结构转扁平数组(反向转换)

将嵌套树结构转换为扁平数组(含 idparentId),适用于前端编辑树后,需要提交扁平数据给后端的场景。

(1)实现方法(基于 DFS 遍历)

javascript
/**
 * 树结构转扁平数组
 * @param {Array} tree - 树结构数组
 * @param {string|number|null} parentId - 父节点 id(用于递归传递)
 * @returns {Array} 转换后的扁平数组
 */
function treeToArray(tree, parentId = null) {
  const flatArray = [];

  // 递归遍历树节点
  function traverse(node, currentParentId) {
    // 1. 提取当前节点信息(移除 children,添加 parentId)
    const { children, ...nodeWithoutChildren } = node;
    flatArray.push({ ...nodeWithoutChildren, parentId: currentParentId });

    // 2. 递归处理子节点(父 id 为当前节点 id)
    if (children && children.length > 0) {
      children.forEach((child) => {
        traverse(child, node.id);
      });
    }
  }

  // 遍历根节点(父 id 为 null)
  tree.forEach((rootNode) => {
    traverse(rootNode, parentId);
  });

  return flatArray;
}

(2)使用示例

javascript
// 使用上面转换后的 tree 进行反向转换
const flatArrayAgain = treeToArray(tree);
console.log("树转数组结果:", flatArrayAgain);

输出结果(与原始扁平数组一致):

javascript
[
  { id: 1, name: "技术部", parentId: null },
  { id: 11, name: "前端组", parentId: 1 },
  { id: 111, name: "张三", parentId: 11 },
  { id: 112, name: "李四", parentId: 11 },
  { id: 12, name: "后端组", parentId: 1 },
  { id: 121, name: "王五", parentId: 12 },
  { id: 2, name: "产品部", parentId: null },
  { id: 21, name: "赵六", parentId: 2 },
];

3. 特殊场景处理

(1)处理不规范数据(如缺失 parentId 或循环引用)

javascript
function safeArrayToTree(flatArray, rootParentId = null) {
  const nodeMap = new Map();
  const invalidNodes = []; // 存储无效节点(如无 id 或 parentId 不存在)

  // 1. 过滤无效节点(无 id 的节点)
  flatArray.forEach((item) => {
    if (item.id === undefined || item.id === null) {
      invalidNodes.push(item); // 记录无效节点
      return;
    }
    nodeMap.set(item.id, { ...item, children: [] });
  });

  // 2. 构建树(跳过 parentId 无效的节点)
  const tree = [];
  flatArray.forEach((item) => {
    if (item.id === undefined || item.id === null) return; // 跳过无效节点

    const currentNode = nodeMap.get(item.id);
    const parentId = item.parentId;

    if (parentId === rootParentId) {
      tree.push(currentNode);
    } else {
      const parentNode = nodeMap.get(parentId);
      if (parentNode) {
        parentNode.children.push(currentNode);
      } else {
        invalidNodes.push(currentNode); // 父节点不存在,标记为无效
      }
    }
  });

  // 打印警告信息(可选)
  if (invalidNodes.length > 0) {
    console.warn("存在无效节点(未加入树中):", invalidNodes);
  }

  return tree;
}

(2)带额外字段转换(如保留层级信息)

转换时添加 level 字段记录节点层级:

javascript
function treeToArrayWithLevel(tree) {
  const flatArray = [];

  function traverse(node, parentId, level) {
    const { children, ...rest } = node;
    flatArray.push({ ...rest, parentId, level }); // 新增 level 字段
    if (children && children.length) {
      children.forEach((child) => traverse(child, node.id, level + 1));
    }
  }

  tree.forEach((root) => traverse(root, null, 1)); // 根节点层级为 1
  return flatArray;
}

// 使用示例
const flatWithLevel = treeToArrayWithLevel(tree);
console.log("带层级的扁平数组:", flatWithLevel);

输出结果(包含 level 字段):

javascript
[
  { id: 1, name: "技术部", parentId: null, level: 1 },
  { id: 11, name: "前端组", parentId: 1, level: 2 },
  { id: 111, name: "张三", parentId: 11, level: 3 },
  // ... 其他节点
];

4. 应用场景总结

转换方向应用场景核心逻辑
数组 → 树后端返回扁平数据,前端需要层级展示(如菜单、组织架构)哈希表映射 + 父子关联
树 → 数组前端编辑树结构后,提交数据给后端(通常需要扁平格式)DFS 遍历 + 记录 parentId

总结

  • 数组转树的关键是用哈希表快速查找父节点,避免嵌套循环,提高效率。
  • 树转数组的关键是递归遍历树节点,同时记录每个节点的父 ID。
  • 实际开发中需处理边界情况(如无效节点、循环引用),保证转换稳定性。

以上方法可直接用于生产环境,根据业务需求调整字段(如用 key 代替 idpid 代替 parentId)即可。