JS中将数组和对象混合组装为JSON字符串

作者:木棉时间:2026-11-05 16:41:42浏览:177

深入讲解JavaScript中如何将对象与数组灵活混合组装并序列化为标准JSON字符串。涵盖多层嵌套结构设计、动态组装模式、JSON.stringify核心参数用法及常见边界陷阱防范。

JavaScript 教程 主题:复合数据组装与序列化 目标输出:标准 JSON 文本
JS中将数组和对象混合组装为JSON字符串
图 0:JavaScript 内存复合数据结构向标准 JSON 字符串的装配与序列化全景
在 JavaScript 中将数组和对象混合组装为 JSON 字符串,本质上分为两步:第一步在 JS 运行内存中构建合法的嵌套引用结构(对象包含数组,或数组包含对象);第二步使用内置的 JSON.stringify() 方法将其序列化为符合 RFC 8259 规范的纯文本字符串。掌握这一操作的关键,在于理清复合数据的层级关系、熟练应用动态组装模式,并妥善处理循环引用与特殊类型的序列化边界。

一、认知核心结构:对象与数组的嵌套形态

在实际业务中(例如向后端提交复合订单、上报埋点日志或持久化前端状态),单一的键值对对象或简单的扁平数组通常无法满足需求。现代 Web 接口契约几乎全部建立在“对象与数组深度混合”的模型之上。

要顺畅地组装数据,首先需要明确两者在 JavaScript 内存中的定位差异:

  • 普通对象(Object):无序的键值对字典(Key-Value),适合用于描述具体的“实体”,例如单个用户、订单元数据、分页控制参数。键名在底层为字符串或 Symbol。
  • 数组(Array):有序的索引列表(Index-based),适合用于表达相同类型实体的“集合”,例如商品清单、权限列表、多选标签集合。

常见的混合嵌套形态主要有以下两种:

  1. 外层为对象,内层属性挂载数组:最经典的 API 载荷模型。顶层对象负责保存状态元信息(如 batchId、timestamp),其某个属性的值是一个包含多个子对象的数组。
  2. 外层为数组,内部填充多个独立对象:批量操作模型。整个数据容器是一个队列,其中的每个元素都是一个拥有完整属性的对象。
JS中将数组和对象混合组装为JSON字符串
图 1:对象与数组的层级嵌套模型(树形容器结构)

二、装配模式:动态构建复合数据结构的常用技术

在 JavaScript 运行环境中组装混合数据结构,并不是直接拼接文本,而是操作内存中的对象与数组引用。组装完成之后,统一交给序列化器输出。根据业务场景的不同,主要有三种实现方式。

1. 过程式分步装配(属性赋值与 Array.prototype.push)

当数据来源于多步骤用户输入(例如向导式表单)、异步分页请求或条件分支计算时,分步装配是最直观的方式。先声明顶层框架,再根据逻辑动态添加属性与子数组元素。

方式一:分步过程式组装示例
// 1. 初始化顶层容器对象
const payload = {
  version: "1.0",
  createdAt: new Date().toISOString(),
  records: [] // 预留空数组
};

// 2. 动态追加基础属性
payload.operatorId = "USR-8802";
payload.auditPassed = true;

// 3. 循环或按需向内层数组追加子对象
const rawDataList = [
  { rawCode: "A01", count: 5 },
  { rawCode: "B03", count: 12 }
];

for (const item of rawDataList) {
  // 组装并清洗子项对象
  const cleanItem = {
    itemCode: item.rawCode,
    quantity: item.count,
    subtotal: item.count * 10.5
  };
  payload.records.push(cleanItem);
}

// 4. 序列化为 JSON 字符串
const jsonResult = JSON.stringify(payload);
console.log(jsonResult);

2. 声明式字面量与解构扩展运算符(Spread Syntax)

当数据源已经具备雏形,需要对其进行增量整合或合并多个数据段时,利用 ES6 的对象扩展运算符 ... 和数组扩展运算符可以编写出不可变、声明式的组装代码,避免直接修改原对象。

方式二:扩展运算符组合示例
const baseConfig = { env: "production", timeout: 3000 };
const dynamicServices = [
  { name: "auth", port: 8081 },
  { name: "gateway", port: 8080 }
];

// 使用字面量直接完成混合装配
const mergedPayload = {
  ...baseConfig,
  cluster: "east-asia",
  // 数组合并与元素注入
  services: [
    ...dynamicServices,
    { name: "monitor", port: 9090, enabled: true }
  ],
  meta: {
    generatedBy: "system-builder",
    timestamp: Date.now()
  }
};

const jsonString = JSON.stringify(mergedPayload);

3. 函数式管道加工(Array.prototype.map 与 reduce)

处理批量业务列表时,最规范的实践是通过 .map() 纯函数将原始列表投影转换为符合目标规范的对象数组,然后再塞入父对象容器中。

方式三:结合 map 批量处理并组装
const selectedProducts = [
  { id: 101, title: "机械键盘", price: 299, inStock: true },
  { id: 102, title: "人体工学椅", price: 1280, inStock: true }
];

// 集中装配:外部对象直接包裹处理后的数组
const exportData = {
  exportTime: new Date().toISOString(),
  totalCount: selectedProducts.length,
  // 通过 map 过滤掉不需要的 inStock 字段,并注入折后计算值
  productList: selectedProducts.map(p => ({
    productId: p.id,
    productName: p.title,
    retailPrice: p.price,
    discountPrice: Number((p.price * 0.9).toFixed(2))
  }))
};

const finalJson = JSON.stringify(exportData);
JS中将数组和对象混合组装为JSON字符串
图 2:从内存实体装配到 JSON 文本输出的完整生命周期

三、序列化核心:使用 JSON.stringify 转换为标准字符串

完成内存对象的组装后,必须调用 JSON.stringify() 将其转为符合 JSON 规范的字符串。JSON.stringify 拥有三个参数,完整签名如下:

JSON.stringify(value[, replacer[, space]])

1. 基础调用与输出压缩

最直接的用法是只传递第一个参数。它会递归遍历对象与数组的所有嵌套属性,将其展开并转译成没有额外空格和换行的紧凑型字符串。这种格式体积最小,是网络 HTTP 请求体传输的标准形式。

基础调用形式
const rawStr = JSON.stringify(mergedPayload);
// 输出结果紧凑无空格:{"version":"1.0","createdAt":"2026-11-03T...","records":[{"itemCode":"A01",...}]}

2. 格式化排版:使用 space 参数增强可读性

第三个参数 space 控制输出的缩进与排版。如果传入一个正整数(例如 2 或 4),序列化器就会在每一级嵌套对象或数组项后换行并缩进对应数量的空格;也可以传入制表符字符串 " "。这在控制台打印调试、本地持久化配置文件时极其有用。

排版输出对比
// 缩进 2 个空格输出
const readableStr = JSON.stringify(mergedPayload, null, 2);
console.log(readableStr);
/*
{
  "version": "1.0",
  "records": [
    {
      "itemCode": "A01",
      "quantity": 5
    }
  ]
}
*/

3. 精准过滤:利用 replacer 控制字段序列化

第二个参数 replacer 允许精准拦截和定制序列化行为。它支持两种形式:

  • 数组形式(白名单):只保留数组中指定的键名。注意:数组白名单只对对象的键名有效,对数组内部的元素索引无效。
  • 函数形式(转换器):接收 (key, value),在每次访问对象属性或数组元素时触发。返回 undefined 表示过滤丢弃该项,返回其他值则替换原值。
使用 replacer 进行字段脱敏与过滤
const complexData = {
  id: 1001,
  token: "secret-xyz-token",
  tags: ["vip", "verified"],
  profile: { name: "张工", age: 32, internalScore: 99 }
};

// 1. 白名单模式:仅导出特定键
const whiteListJson = JSON.stringify(complexData, ["id", "tags", "profile", "name"]);
// 输出中将自动剔除 token、age、internalScore

// 2. 函数过滤模式:排除敏感字段并加工数据
const safeJson = JSON.stringify(complexData, (key, value) => {
  if (key === "token" || key === "internalScore") {
    return undefined; // 丢弃敏感属性
  }
  if (typeof value === "string") {
    return value.trim(); // 去除空白字符
  }
  return value; // 其余原样返回
}, 2);
JS中将数组和对象混合组装为JSON字符串
图 3:JS 运行时实体与标准 JSON 规范的差异与转换映射

四、边界陷阱与容错防御方案

在混合组装多层数据时,由于数据往往来自接口、DOM 表单或用户输入,极易夹杂特殊值或异常引用。若不加防御,直接调用 JSON.stringify 会引发静默丢字段或直接抛出未捕获异常。

1. 陷阱一:undefined、函数与 Symbol 的不一致行为

JSON 规范中没有 undefined、函数(Function)或 Symbol。当把包含这些类型的结构转换为 JSON 时:

  • 在对象属性中:该键值对会被完全忽略并自动剔除。
  • 在数组元素中:为了保证数组索引和长度的稳定性,这些值会被强制转换为 null。
  • 作为顶层单独值:直接返回 undefined(不是字符串 "undefined")。
边界对比测试
const testData = {
  regularField: "active",
  missingObjValue: undefined,   // 在对象中会被忽略剔除
  callbackFn: () => {},        // 在对象中会被忽略剔除
  list: [
    "first",
    undefined,                 // 在数组中转换为 null
    () => {},                  // 在数组中转换为 null
    "last"
  ]
};

console.log(JSON.stringify(testData));
// 输出结果:
// {"regularField":"active","list":["first",null,null,"last"]}
提示:若后端接口强制要求某些字段传 null 占位,前端不能将对象属性赋值为 undefined,而应显式赋予 null(例如 item.parent = null;)。

2. 陷阱二:循环引用(Circular Reference)引发崩溃

在对象与数组混合组装时,如果某个子对象重新引用了父对象,就会形成环(Loop)。此时执行 JSON.stringify() 会直接抛出:
TypeError: Converting circular structure to JSON

循环引用报错示例
const parent = { name: "根节点", children: [] };
const child = { id: 101, parentRef: parent }; // 子对象反向引用父级
parent.children.push(child);

// 直接序列化将立即抛出 TypeError 异常!
// JSON.stringify(parent); 

防御策略:利用 WeakSet 记录已经访问过的对象引用。在 replacer 中若检测到当前对象已被访问过,则返回占位符或 undefined 打破闭环。

使用 WeakSet 实现安全的去环序列化函数
function safeStringify(data, space = 0) {
  const seen = new WeakSet();
  return JSON.stringify(data, (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        return "[Circular]"; // 检测到循环引用,安全替换
      }
      seen.add(value);
    }
    return value;
  }, space);
}

// 正常运行,不再崩溃:
console.log(safeStringify(parent, 2));

3. 陷阱三:BigInt 导致不可捕获异常

JavaScript 的原生 BigInt 类型不能被默认序列化。如果数据中包含如 10000000000000001n 的数值,调用 JSON.stringify() 会抛出 TypeError: Do not know how to serialize a BigInt。

解决方案:在组装数据时,主动使用 item.bigId.toString() 转为字符串传输;或者为 BigInt.prototype.toJSON 提供全局垫片:
BigInt.prototype.toJSON = function() { return this.toString(); };
JS中将数组和对象混合组装为JSON字符串
图 4:复合数据结构安全组装与序列化容错决策全流程

五、完整实践:电商复杂订单报文组装与验证

为了检验上述知识在真实场景中的应用,下面提供一段包含主单信息、收货地址对象、商品明细数组、费用结算及敏感字段过滤的完整可运行代码。

实战:电商订单复合报文组装与序列化
/**
 * 组装电商订单复合提交报文
 * 包含:顶层对象、子对象(address)、子对象数组(items)、标量数组(couponCodes)
 */
function buildOrderPayload(orderInfo, rawItems, address) {
  // 1. 组装子对象数组并做安全计算
  const processedItems = rawItems.map((item, index) => {
    const unitPrice = Number(item.price) || 0;
    const qty = parseInt(item.quantity, 10) || 1;
    return {
      index: index + 1,
      skuId: String(item.skuId).trim(),
      title: item.title,
      unitPrice: unitPrice,
      quantity: qty,
      subtotal: Number((unitPrice * qty).toFixed(2)),
      tags: Array.isArray(item.tags) ? [...item.tags] : [] // 保证为纯数组
    };
  });

  // 2. 计算订单总金额
  const totalAmount = processedItems.reduce((sum, item) => sum + item.subtotal, 0);

  // 3. 组装顶层复合载体
  const orderPayload = {
    orderId: "ORD-" + Date.now(),
    createTime: new Date().toISOString(),
    status: "CREATED",
    settlement: {
      currency: "CNY",
      totalAmount: Number(totalAmount.toFixed(2)),
      discountAmount: 20.00,
      actualPay: Number(Math.max(0, totalAmount - 20).toFixed(2))
    },
    shippingAddress: {
      receiver: address.receiver,
      phone: address.phone,
      province: address.province,
      city: address.city,
      detail: address.detail,
      zipCode: address.zipCode || null // 显式置空而非 undefined
    },
    couponCodes: ["NEW_USER_20", "FALL_DISCOUNT"],
    items: processedItems,
    notes: orderInfo.notes || ""
  };

  return orderPayload;
}

// 4. 模拟测试数据并执行
const mockAddress = {
  receiver: "赵工",
  phone: "13800138000",
  province: "浙江省",
  city: "杭州市",
  detail: "西湖区科技园路88号"
};

const mockItems = [
  { skuId: "SKU-4001", title: "4K显示器", price: 1899.00, quantity: 1, tags: ["数码", "办公"] },
  { skuId: "SKU-9902", title: "Type-C拓展坞", price: 159.00, quantity: 2, tags: ["配件"] }
];

const orderObj = buildOrderPayload({ notes: "请尽量工作日送达" }, mockItems, mockAddress);

// 5. 序列化为最终的 JSON 字符串
const orderJsonString = JSON.stringify(orderObj, null, 2);

console.log("----- 组装完成的 JSON 字符串 -----");
console.log(orderJsonString);

运行验证与输出结果示例

执行上述代码后,通过 typeof orderJsonString 可以检测到其返回值类型为严格的 "string"。输出内容如下(经 space = 2 格式化):

{
  "orderId": "ORD-1793786000000",
  "createTime": "2026-11-03T09:52:00.000Z",
  "status": "CREATED",
  "settlement": {
    "currency": "CNY",
    "totalAmount": 2217,
    "discountAmount": 20,
    "actualPay": 2197
  },
  "shippingAddress": {
    "receiver": "赵工",
    "phone": "13800138000",
    "province": "浙江省",
    "city": "杭州市",
    "detail": "西湖区科技园路88号",
    "zipCode": null
  },
  "couponCodes": [
    "NEW_USER_20",
    "FALL_DISCOUNT"
  ],
  "items": [
    {
      "index": 1,
      "skuId": "SKU-4001",
      "title": "4K显示器",
      "unitPrice": 1899,
      "quantity": 1,
      "subtotal": 1899,
      "tags": [
        "数码",
        "办公"
      ]
    },
    {
      "index": 2,
      "skuId": "SKU-9902",
      "title": "Type-C拓展坞",
      "unitPrice": 159,
      "quantity": 2,
      "subtotal": 318,
      "tags": [
        "配件"
      ]
    }
  ],
  "notes": "请尽量工作日送达"
}

在将该字符串作为 fetch() 的 body 提交给后端服务时,记得设置请求头 Content-Type: application/json;charset=utf-8,即可确保后端不论是 Node.js、Java、Go 还是 Python,都能精准无歧义地逆向反序列化还原数据。

作者最新文章
相关文章
更多