导读:本期聚焦于Ada创作的《如何将嵌套对象中各层级的键值对扁平合并为单个对象(无嵌套结构)》,敬请观看详情。在前端开发场景中,我们经常会遇到需要处理嵌套对象的情况,比如接口返回的多层嵌套数据需要转换为扁平结构方便后续使用。很多开发者不清楚如何高效将嵌套对象中各层级的键值对合并为无嵌套的单层对象,本文会介绍两种常用的实现思路,分别适配不同复杂度的嵌套场景,同时给出完整的代码示例和逻辑说明,帮助开发者快速掌握对象扁平化的实现方法,解决实际开发中的数据格式转换问题。

在数据处理、接口适配、配置读取、日志上报等开发任务中,经常需要面对多层嵌套的对象结构。嵌套结构能够自然地表达层级关系,但在进行属性查找、字段比对、缓存存储、表格渲染或生成查询参数时,深层路径往往会让代码变得繁琐。为了解决这个问题,可以把嵌套对象中不同层级的键值对合并到一个单层对象中,使每个属性都拥有明确、完整且可直接访问的键名。

嵌套对象扁平化的核心目标

所谓扁平合并,并不是简单地把所有值复制到同一个对象里,而是要在合并过程中保留原始数据的路径信息。例如,原始对象中的 address.city 所对应的值,在扁平化之后通常会变成一个带有路径含义的新键,如 address_city。这样既消除了嵌套结构,又让开发者能够从键名中看出该字段原本所属的层级。

这种处理方式的价值在于,它可以把复杂的树形数据转换成更容易遍历、更容易序列化的键值集合。当所有字段都位于同一层时,可以使用统一的循环逻辑读取属性,也可以更方便地把数据写入缓存、生成表单字段、构造搜索索引或者进行差异比较。对于需要按字段名定位数据的场景,扁平结构通常比深层嵌套更稳定。

从实现角度看,扁平合并的核心步骤包括三个部分:第一,逐层访问对象中的每个属性;第二,为每一层属性拼接一个能够表达路径的新键名;第三,判断当前属性值是否仍然需要继续展开。如果属性值仍然是对象或数组,就继续向下处理;如果已经是字符串、数字、布尔值等叶子值,就把它写入最终结果。

纯对象嵌套的递归处理方式

如果数据结构只包含普通对象嵌套,而不包含数组、Map、Set 等复杂类型,递归是最容易理解的实现方式。递归函数会从顶层对象开始,依次读取每个属性。如果属性值仍然是普通对象,就把当前属性名作为路径的一部分传入下一层;如果属性值已经是最终值,就直接写入结果对象。

在这种实现中,前缀参数非常重要。它保存了当前属性之前已经经过的路径。每进入一层,就把当前键名追加到前缀后面。为了避免键名冲突,通常会选择一个分隔符,如下划线、点号或者中划线。分隔符本身没有绝对标准,关键是要与业务中的字段命名规则兼容,避免生成歧义路径。

下面的示例展示了一个基础的递归实现。该示例只处理普通对象嵌套,当属性值不是普通对象时,会将其作为叶子值保存。为了便于观察结果,代码使用下划线连接不同层级的键名。

// 将纯对象嵌套结构扁平化为单层对象
function flattenObject(source, prefix = '', result = {}) {
  // 如果传入值不是对象,直接作为叶子值写入结果
  if (source === null || typeof source !== 'object') {
    if (prefix) {
      result[prefix] = source;
    }
    return result;
  }

  // 基础场景只展开普通对象,数组不继续展开
  if (Array.isArray(source)) {
    if (prefix) {
      result[prefix] = source;
    }
    return result;
  }

  // 遍历对象自身属性
  for (const key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      const nextKey = prefix ? prefix + '_' + key : key;
      const value = source[key];

      // 普通对象继续递归,其他值直接写入结果
      if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
        flattenObject(value, nextKey, result);
      } else {
        result[nextKey] = value;
      }
    }
  }

  return result;
}

const profile = {
  name: '张三',
  age: 28,
  address: {
    province: '广东',
    city: '深圳',
    detail: {
      street: '科技路',
      number: 100
    }
  }
};

console.log(flattenObject(profile));
// 输出:
// {
//   name: '张三',
//   age: 28,
//   address_province: '广东',
//   address_city: '深圳',
//   address_detail_street: '科技路',
//   address_detail_number: 100
// }

从输出结果可以看到,原本位于 address.detail.street 路径下的值,被转换成了 address_detail_street 这样的单层键名。这样做之后,外部代码不需要再逐层判断中间对象是否存在,只需要通过一个明确键名即可读取目标值。

包含数组的混合结构如何保持键路径清晰

实际项目中的嵌套结构往往不会只有对象,很多字段会包含数组。例如,一个订单对象可能包含多个子项,一个用户资料可能包含多个标签,一个配置对象可能包含多组规则。如果扁平化函数不能识别数组,就可能把数组整体当作叶子值保存,从而失去数组内部的结构信息。

为了处理数组,需要把数组索引作为路径的一部分。常见的做法是将数组下标拼接进键名,例如 list_0_title 表示 list 数组第一项中的 title 字段,info_tags_1 表示 info.tags 数组中的第二项。这样即使数据被展开成单层对象,数组元素的顺序和归属关系仍然可以从键名中还原出来。

下面这段代码在基础递归逻辑上增加了对数组的判断。当遇到数组时,遍历数组中的每一项,并把索引加入当前路径;当遇到对象时,继续按照属性名拼接路径;当遇到基本类型时,将值写入结果。

// 支持对象和数组混合嵌套的扁平化函数
function flattenMixed(source, prefix = '', result = {}) {
  // 处理数组:将索引作为路径的一部分
  if (Array.isArray(source)) {
    source.forEach(function(item, index) {
      const nextKey = prefix ? prefix + '_' + index : String(index);

      if (item !== null && typeof item === 'object') {
        flattenMixed(item, nextKey, result);
      } else {
        result[nextKey] = item;
      }
    });
    return result;
  }

  // 处理普通对象:将属性名作为路径的一部分
  if (source !== null && typeof source === 'object') {
    for (const key in source) {
      if (Object.prototype.hasOwnProperty.call(source, key)) {
        const nextKey = prefix ? prefix + '_' + key : key;
        const value = source[key];

        if (value !== null && typeof value === 'object') {
          flattenMixed(value, nextKey, result);
        } else {
          result[nextKey] = value;
        }
      }
    }
    return result;
  }

  // 基本类型作为叶子值写入结果
  if (prefix) {
    result[prefix] = source;
  }

  return result;
}

const order = {
  id: 1,
  list: [
    { title: '第一项', value: 10 },
    { title: '第二项', value: 20 }
  ],
  info: {
    owner: '李四',
    tags: ['前端', 'JavaScript']
  }
};

console.log(flattenMixed(order));
// 输出:
// {
//   id: 1,
//   list_0_title: '第一项',
//   list_0_value: 10,
//   list_1_title: '第二项',
//   list_1_value: 20,
//   info_owner: '李四',
//   info_tags_0: '前端',
//   info_tags_1: 'JavaScript'
// }

这种混合结构的扁平化方式特别适合用于生成表单字段名、构造动态校验规则或者导出结构化数据。因为数组索引被显式写入键名,所以即使原始数据包含多个相似对象,展开后的字段也不会互相覆盖。

使用迭代栈降低深层递归风险

递归写法的优点是逻辑清晰,接近树形结构本身的定义。不过,当嵌套层级非常深,或者对象中存在大量连续嵌套节点时,递归调用栈可能不断增长,进而带来栈溢出风险。虽然普通业务数据通常不会达到危险深度,但在处理自动生成的大型结构、深层配置树或复杂元数据时,仍然值得提前考虑更稳妥的方案。

迭代实现的核心思想是不再依赖函数调用栈,而是手动维护一个待处理任务集合。每个任务包含两部分信息:当前要处理的数据值,以及该数据值对应的键名前缀。程序从栈中取出一个任务,判断它是数组、对象还是叶子值。如果仍然是可展开结构,就把下一层任务重新压入栈中;如果已经是叶子值,就写入结果对象。

下面的示例使用栈来模拟递归过程。它支持对象和数组混合结构,同时避免递归调用带来的深层调用栈。对于需要处理未知深度数据的工具函数来说,这种实现更具可控性。

// 使用栈模拟递归,避免深层嵌套造成调用栈过长
function flattenByStack(source) {
  const result = {};
  const stack = [{ value: source, prefix: '' }];

  while (stack.length) {
    const current = stack.pop();
    const value = current.value;
    const prefix = current.prefix;

    // 数组:把索引加入路径后继续处理
    if (Array.isArray(value)) {
      value.forEach(function(item, index) {
        const nextKey = prefix ? prefix + '_' + index : String(index);

        if (item !== null && typeof item === 'object') {
          stack.push({ value: item, prefix: nextKey });
        } else {
          result[nextKey] = item;
        }
      });
    } else if (value !== null && typeof value === 'object') {
      // 对象:把属性名加入路径后继续处理
      for (const key in value) {
        if (Object.prototype.hasOwnProperty.call(value, key)) {
          const nextKey = prefix ? prefix + '_' + key : key;
          const child = value[key];

          if (child !== null && typeof child === 'object') {
            stack.push({ value: child, prefix: nextKey });
          } else {
            result[nextKey] = child;
          }
        }
      }
    } else if (prefix) {
      // 叶子值直接写入结果
      result[prefix] = value;
    }
  }

  return result;
}

const deepConfig = {
  a: {
    b: {
      c: {
        d: {
          e: '深层值'
        }
      }
    }
  },
  list: [
    { name: '甲' },
    { name: '乙' }
  ]
};

console.log(flattenByStack(deepConfig));
// 输出:
// {
//   a_b_c_d_e: '深层值',
//   list_0_name: '甲',
//   list_1_name: '乙'
// }

与递归版本相比,迭代版本的执行流程稍微复杂一些,但它把展开过程变成了显式的循环任务。开发者可以在此基础上继续扩展,例如限制最大处理深度、跳过空对象、过滤特定键名,或者在写入结果前对值进行统一转换。

键名规则、边界情况与工程化建议

在选择扁平化方案时,首先要确定键名生成规则。下划线、点号、中划线都可以作为路径分隔符,但不同规则会影响后续使用方式。如果结果对象要作为表单字段名提交,需要确认后端是否能正确解析;如果结果要参与缓存键生成,则需要避免分隔符与业务字段本身包含的符号冲突。

其次要注意边界情况。nulltypeof 结果也是 object,因此必须单独判断,避免把 null 当作普通对象继续遍历。空对象和空数组也需要明确策略:可以忽略,可以保留占位值,也可以根据业务需求写入特定默认值。此外,如果原始对象中存在循环引用,无论递归还是迭代都需要额外记录已访问节点,否则会导致无限展开。

总体而言,把嵌套对象扁平合并为单个无嵌套对象,本质上是在树形结构和键值表之间建立一种可追踪的映射关系。对于简单对象嵌套,递归实现足够直观;对于包含数组的复杂数据,需要把索引纳入路径;对于层级很深或结构不可控的数据,可以改用栈或队列进行迭代处理。只要根据业务场景选择合适的键名规则和边界处理策略,就能让扁平化结果既便于访问,又保留必要的层级信息。

对象扁平化嵌套对象处理JavaScript键值对合并修改时间:2026-06-28 18:54:35

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。