JS中用JSON.stringify正确生成JSON字符串

作者:木棉时间:2026-11-03 22:22:58浏览:203

深入讲解在 JavaScript 中使用 JSON.stringify 正确生成 JSON 字符串的核心机制,涵盖语法参数、特殊类型转换、循环引用与 BigInt 避坑方案及自定义 toJSON 实战。

JavaScript 权威进阶指南

在 JavaScript 中,将内存中的对象或数据结构转为标准的 JSON 文本,最直接、标准的方式是调用内置的 JSON.stringify() 方法。然而,简单传入一个对象并不总是能得到预期结果:特殊类型(如 undefined、函数、Symbol)会被隐式丢弃或转为 null,遇到 BigInt 或循环引用时会直接抛出运行时异常,而在需要安全日志排查或 API 数据传输时,缺乏对属性过滤与格式化的控制也会导致输出臃肿或信息泄露。本文将完整拆解 JSON.stringify(value, replacer, space) 的三大参数、执行流程、类型转换隐式规则及高频报错场景,并提供安全可落地的序列化方案。

JS中用JSON.stringify正确生成JSON字符串
图 0:JSON.stringify 核心参数解构与处理管线概览

一、基本语法与执行三要素

ECMAScript 规范定义的序列化方法完整签名如下:

JavaScript 标准方法签名ECMAScript 5+
JSON.stringify(value[, replacer[, space]])

该方法接收三个参数,其核心职责分工明确:

  • value [必需]:需要被序列化为 JSON 字符串的 JavaScript 变量、对象或数组。
  • replacer [可选]:属性过滤器或转换函数。既可以传入一个函数逐层修改属性,也可以传入一个字符串或数字数组作为保留属性的白名单。
  • space [可选]:控制缩进空白以提高输出排版可读性的参数。可以传入数字(代表缩进空格数,最大为 10)或字符串(如制表符 )。

最简单的调用场景只需传递第一个参数。若转换成功,它返回一个符合 JSON 语法的 UTF-16 编码字符串;如果不符合 JSON 规范的特定基本类型被单独序列化,则会返回 JavaScript 的原始值 undefined。

示例代码: 基础序列化调用index.js
const user = {
  id: 1001,
  name: "张三",
  isActive: true,
  roles: ["admin", "editor"]
};

const jsonResult = JSON.stringify(user);
console.log(typeof jsonResult); // string
console.log(jsonResult);
示例输出 (单行紧凑文本)
{"id":1001,"name":"张三","isActive":true,"roles":["admin","editor"]}

二、序列化生命周期与内部推导机制

理解 JSON.stringify 的关键在于把握其内部判定流程。当执行序列化时,引擎并不是简单地遍历对象键值,而是会按照严格的生命周期顺序进行多阶段处理:

  1. 根对象包装:引擎首先会建立一个微型的虚拟包装对象 { "": value },以此确保即使顶层是一个基本类型或特殊对象,也能统一经由后续管道处理。
  2. 调用 toJSON() 钩子:检查当前处理的节点对象自身或原型链上是否存在 toJSON 方法。如果存在,立即调用该方法,并将其返回值作为后续步骤的新目标值。例如,内置的 Date 对象正是通过该机制将实例直接转换为 ISO 时间字符串。
  3. 执行 replacer 转换与过滤:如果指定了 replacer 参数,当前键与转换后的值会传递给 replacer。如果 replacer 返回 undefined,该属性会被直接从输出中剔除。
  4. 祖先栈检测循环引用:检查当前对象是否已经在其父级遍历路径的栈结构中存在。如果存在交叉嵌套或自我引用,引擎会直接中断并抛出 TypeError: Converting circular structure to JSON。
  5. 类型特化与格式化输出:根据处理结果的最终类型进行字符串拼装,并根据 space 参数插入换行与缩进空格。
JS中用JSON.stringify正确生成JSON字符串
图 1:JSON.stringify 内部生命周期与四步推导判定流程图

三、深入 replacer 与 space:精准控制输出内容与外观

许多开发者仅使用单个参数,但 replacer 和 space 才是实现工程化数据过滤与排版的利器。

1. replacer 作为数组:属性白名单精确过滤

当 replacer 传入字符串或数字数组时,序列化过程将转变为严格的白名单提取模式。只有数组中声明的键名才会被保留到最终生成的 JSON 文本中,其余属性将被自动忽略。这在向客户端传输数据前过滤敏感字段(如密码、内部状态)时极为方便。

示例代码: 数组白名单过滤filter.js
const account = {
  id: 42,
  username: "alex99",
  passwordHash: "a1b2c3d4e5f6",
  email: "alex@example.com",
  level: "VIP"
};

// 仅保留 id、username 和 email
const publicProfile = JSON.stringify(account, ["id", "username", "email"]);
console.log(publicProfile);
示例输出
{"id":42,"username":"alex99","email":"alex@example.com"}

2. replacer 作为函数:属性值拦截与深度转换

当 replacer 是一个函数时,其签名为 (key, value)。序列化过程中的每一个属性都会触发该函数。需要特别牢记的是:初次调用时,传入的 key 是空字符串 "",value 则是整个被序列化的根对象。

如果函数返回 undefined,该键值对将从对象中剔除;如果返回其它值,该返回值将替换原先的属性值。

示例代码: 函数式 replacer 过滤与转换replacer_fn.js
const product = {
  title: "无线键盘",
  price: 299,
  secretInventoryCode: "WH-8848",
  inStock: true
};

const sanitized = JSON.stringify(product, (key, value) => {
  // 必须保留根对象,否则整个转换将被中止
  if (key === "") return value;
  
  // 过滤包含 secret 的属性
  if (key.includes("secret")) return undefined;
  
  // 给价格添加货币符号描述
  if (typeof value === "number") return `¥${value}`;
  
  return value;
});

console.log(sanitized);
示例输出
{"title":"无线键盘","price":"¥299","inStock":true}

3. space 参数的美化排版控制

未传 space 时,生成的字符串没有任何换行或缩进,体积最小,适合网络传输。但在保存本地配置文件、输出控制台日志或展示给用户调试时,可读性极差。space 支持两种取值:

  • 数字(1 到 10):表示每一级嵌套缩进对应数量的空格。若传入数值大于 10,引擎会自动限制为 10;若小于 1 则视为不缩进。
  • 字符串:每一层缩进使用该字符串填充(最长截取前 10 个字符)。常见做法是传入制表符 " "。
示例代码: 缩进美化输出pretty.js
const config = { name: "App", version: "1.0.0", env: "production" };

// 使用 2 个空格缩进美化
const formattedJson = JSON.stringify(config, null, 2);
console.log(formattedJson);
示例输出 (带换行与空格缩进)
{ "name": "App", "version": "1.0.0", "env": "production" }
JS中用JSON.stringify正确生成JSON字符串
图 2:replacer 白名单与拦截器机制配合 space 缩进控制架构图

四、特殊数据类型的转换规则与静默陷阱

由于 JSON 是一种独立于语言的文本协议,其语法仅原生支持 string、number、boolean、null、object 和 array 六类结构。当 JavaScript 特有的数据类型进入 JSON.stringify 时,会触发规范规定的降级或忽略策略。如果对其行为不了解,极易出现数据静默丢失的隐蔽 Bug。

1. undefined、函数与 Symbol 的三大宿主表现

这三种类型在 JSON 中均无对应表示,引擎处理它们遵循严格的宿主上下文判定:

  • 作为对象的属性值时:该属性会被直接静默忽略,不出现在生成的键值对中。
  • 作为数组的元素时:为了保持数组索引下标的连续性与完整性,它们会被统一转换为 null。
  • 作为单独值被转换时:直接返回 JavaScript 原始值 undefined,而不是字符串 "undefined"。
示例代码: undefined、函数与 Symbol 的表现差异types_test.js
const testObj = {
  valid: "OK",
  missing: undefined,
  action: function() { return 1; },
  sym: Symbol("uid")
};
console.log(JSON.stringify(testObj)); 
// 输出: {"valid":"OK"} (属性全部丢失)

const testArr = ["OK", undefined, function() {}, Symbol("arr")];
console.log(JSON.stringify(testArr)); 
// 输出: ["OK",null,null,null] (转为 null 占位)

console.log(JSON.stringify(undefined)); // undefined (原始值,非字符串)
console.log(JSON.stringify(() => {})); // undefined

2. 非法数值 NaN 与 Infinity 转换为 null

JSON 规范中的数字必须为合法的有限实数,不包含 NaN、Infinity 或 -Infinity。在执行序列化时,它们无论出现在对象属性、数组还是单独调用中,都会被统一转化为 null。

示例代码: 非法数值的序列化转换nan.js
console.log(JSON.stringify({ a: NaN, b: Infinity, c: -Infinity }));
// 输出: {"a":null,"b":null,"c":null}

3. 内置对象的特殊表现:Date、RegExp、Set 与 Map

  • Date 对象:内置实现了 Date.prototype.toJSON(),返回格式为 2026-10-31T15:43:00.000Z 的 ISO 8601 字符串。在调用 JSON.parse 时不会自动恢复为 Date 实例,而会保持字符串形态。
  • RegExp 与 Error 对象:默认没有实现特殊的序列化逻辑,仅会序列化对象本身的可枚举属性。因此通常会输出空对象 {}。
  • Set 与 Map:Map 与 Set 的键值对保存在内部槽中,并非普通的可枚举自有属性,直接序列化均会得到空对象 {}。需先行通过 Array.from() 或 Object.fromEntries() 转为数组或对象。
JS中用JSON.stringify正确生成JSON字符串
图 3:JavaScript 核心数据类型在不同容器中的序列化表现对照矩阵

五、致命报错场景与安全解决对策

通常情况下 JSON.stringify 会静默降级,但在以下两类典型场景下会直接抛出不可恢复的运行时错误(TypeError),若未做异常兜底,可能导致服务崩溃或前端应用白屏。

1. 循环引用异常:TypeError: Converting circular structure to JSON

当对象属性直接或间接引用自身或祖先节点时,形成闭环引用。引擎递归遍历无法收敛,因此抛出异常。常见的成环场景包括:父子级树节点对象互相持有、Vue/React 组件实例、包含 DOM 节点的上下文数据等。

典型成环报错代码示例
const parent = { name: "根节点" };
const child = { name: "子节点", parent: parent };
parent.child = child; // 形成循环依赖

JSON.stringify(parent);
// 抛出: Uncaught TypeError: Converting circular structure to JSON

解决策略:利用 WeakSet 跟踪并隔离成环节点

可以通过自定义 replacer 函数,使用 WeakSet 记录已经历过的对象引用。一旦再次遇到相同引用,将其替换为标记文本(如 "[Circular]")或返回 undefined 将其剪除。

解决方案: 循环引用安全序列化函数cycle_safe.js
function stringifyWithoutCircular(obj, space = 2) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        return "[Circular]"; // 检测到成环,替换为标记字符串
      }
      seen.add(value);
    }
    return value;
  }, space);
}

console.log(stringifyWithoutCircular(parent));
示例输出 (成功避免抛错)
{ "name": "根节点", "child": { "name": "子节点", "parent": "[Circular]" } }

2. BigInt 序列化崩溃:TypeError: Do not know how to serialize a BigInt

由于 JSON 规范制定时 JavaScript 尚未引入任意精度大整数 BigInt,标准规范并未给 BigInt 定义默认序列化格式(若强行转为数字可能因精度溢出失真,若转为字符串则改变了原始类型)。因此,引擎对 BigInt 的默认处理是直接抛出 TypeError。

典型 BigInt 崩溃代码示例
const payload = { txId: 9007199254740993n };
JSON.stringify(payload);
// 抛出: Uncaught TypeError: Do not know how to serialize a BigInt

解决策略 A:在 replacer 中做类型转字符串降级(推荐,不污染全局)

解决方案: replacer 转换 BigIntbigint_safe.js
const payload = { orderId: 10001n, amount: 200n };

const safeJson = JSON.stringify(payload, (key, value) =>
  typeof value === "bigint" ? value.toString() : value
);
console.log(safeJson); // {"orderId":"10001","amount":"200"}

解决策略 B:扩展 BigInt 原型添加 toJSON(全局生效,谨慎使用)

解决方案: 原型扩展 toJSONbigint_prototype.js
// 仅在明确整个项目均接受大整数按字符串序列化时使用
BigInt.prototype.toJSON = function() {
  return this.toString();
};

console.log(JSON.stringify({ id: 888888888888888888n })); 
// 输出: {"id":"888888888888888888"}

六、自定义序列化:toJSON 方法的最佳实践

在面向对象或复杂领域模型开发中,通过在类中实现 toJSON 方法,可以赋予实例自主决定序列化数据的能力。

当一个对象拥有 toJSON() 方法时,JSON.stringify 会优先调用该方法,并用其返回的值作为该属性的实际序列化内容。执行顺序为:自身 toJSON() -> replacer 拦截器 -> 最终字符拼装。

示例代码: 类定义中封装 toJSONdomain_model.js
class Member {
  constructor(id, realName, passwordHash, cardNo) {
    this.id = id;
    this.realName = realName;
    this.passwordHash = passwordHash;
    this.cardNo = cardNo;
  }

  // 自定义序列化逻辑,脱敏敏感信息
  toJSON() {
    return {
      id: this.id,
      name: this.realName,
      // 银行卡号掩码脱敏
      cardMask: this.cardNo.replace(/^(\d{4})\d+(\d{4})$/, "$1****$2")
    };
  }
}

const member = new Member(88, "王五", "hash_secret_x99", "6222021234567890");
console.log(JSON.stringify(member));
示例输出 (passwordHash 被隔离,卡号自动脱敏)
{"id":88,"name":"王五","cardMask":"6222****7890"}

七、工程实践:封装健壮的 safeStringify

在大型项目、微服务日志收集或第三方数据上报场景中,直接调用裸露的 JSON.stringify 是极不安全的。下面提供一个兼顾防循环引用崩溃、支持 BigInt 转换以及保证运行稳定的工业级封装函数:

工业级封装: safeStringify 完整实现safeStringify.js
/**
 * 安全序列化函数:彻底解决循环引用、BigInt 崩溃与异常中断问题
 * @param {*} value 待序列化数据
 * @param {Function|Array} [customReplacer] 自定义 replacer 拦截器
 * @param {number|string} [space=0] 缩进排版控制
 * @param {string} [fallback=""] 序列化失败时的回退内容
 * @returns {string} JSON 文本或 fallback 内容
 */
function safeStringify(value, customReplacer = null, space = 0, fallback = "") {
  const seen = new WeakSet();

  try {
    return JSON.stringify(value, (key, val) => {
      // 1. 处理 BigInt 兼容转换
      let currentVal = val;
      if (typeof currentVal === "bigint") {
        currentVal = currentVal.toString();
      }

      // 2. 检测并解除循环引用
      if (typeof currentVal === "object" && currentVal !== null) {
        if (seen.has(currentVal)) {
          return "[Circular]";
        }
        seen.add(currentVal);
      }

      // 3. 执行用户自定义 replacer
      if (typeof customReplacer === "function") {
        return customReplacer(key, currentVal);
      }

      return currentVal;
    }, space);
  } catch (err) {
    // 捕获极端不可恢复异常(如调用 toJSON 内部代码抛错)
    console.error("safeStringify 转换失败:", err);
    return fallback;
  }
}
JS中用JSON.stringify正确生成JSON字符串
图 4:safeStringify 工业级防御架构与排查决策树总结
关键检查维度 默认行为 推荐最佳实践
循环引用 (Circular Ref) 抛出 TypeError 致命异常 使用 WeakSet 记录访问栈,成环节点替换为 "[Circular]"
大整数 (BigInt) 抛出 TypeError 致命异常 在 replacer 中显式转换为字符串 val.toString()
缺失值 (undefined/函数/Symbol) 对象属性静默丢失,数组元素转为 null 涉及关键结构时,在数据层提供显式默认值或 null
敏感信息过滤 全量可枚举属性输出 传递 replacer 数组白名单或使用领域模型内建 toJSON()
网络传输 vs 本地调试 默认紧凑单行无空白 生产环境传输保持 space=null,本地调试或配置文件持久化使用 space=2
作者最新文章
相关文章
更多