JS中用JSON.stringify正确生成JSON字符串
深入讲解在 JavaScript 中使用 JSON.stringify 正确生成 JSON 字符串的核心机制,涵盖语法参数、特殊类型转换、循环引用与 BigInt 避坑方案及自定义 toJSON 实战。
在 JavaScript 中,将内存中的对象或数据结构转为标准的 JSON 文本,最直接、标准的方式是调用内置的 JSON.stringify() 方法。然而,简单传入一个对象并不总是能得到预期结果:特殊类型(如 undefined、函数、Symbol)会被隐式丢弃或转为 null,遇到 BigInt 或循环引用时会直接抛出运行时异常,而在需要安全日志排查或 API 数据传输时,缺乏对属性过滤与格式化的控制也会导致输出臃肿或信息泄露。本文将完整拆解 JSON.stringify(value, replacer, space) 的三大参数、执行流程、类型转换隐式规则及高频报错场景,并提供安全可落地的序列化方案。
一、基本语法与执行三要素
ECMAScript 规范定义的序列化方法完整签名如下:
JSON.stringify(value[, replacer[, space]])
该方法接收三个参数,其核心职责分工明确:
- value [必需]:需要被序列化为 JSON 字符串的 JavaScript 变量、对象或数组。
- replacer [可选]:属性过滤器或转换函数。既可以传入一个函数逐层修改属性,也可以传入一个字符串或数字数组作为保留属性的白名单。
- space [可选]:控制缩进空白以提高输出排版可读性的参数。可以传入数字(代表缩进空格数,最大为 10)或字符串(如制表符
)。
最简单的调用场景只需传递第一个参数。若转换成功,它返回一个符合 JSON 语法的 UTF-16 编码字符串;如果不符合 JSON 规范的特定基本类型被单独序列化,则会返回 JavaScript 的原始值 undefined。
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 的关键在于把握其内部判定流程。当执行序列化时,引擎并不是简单地遍历对象键值,而是会按照严格的生命周期顺序进行多阶段处理:
- 根对象包装:引擎首先会建立一个微型的虚拟包装对象
{ "": value },以此确保即使顶层是一个基本类型或特殊对象,也能统一经由后续管道处理。 - 调用 toJSON() 钩子:检查当前处理的节点对象自身或原型链上是否存在
toJSON方法。如果存在,立即调用该方法,并将其返回值作为后续步骤的新目标值。例如,内置的Date对象正是通过该机制将实例直接转换为 ISO 时间字符串。 - 执行 replacer 转换与过滤:如果指定了
replacer参数,当前键与转换后的值会传递给replacer。如果replacer返回undefined,该属性会被直接从输出中剔除。 - 祖先栈检测循环引用:检查当前对象是否已经在其父级遍历路径的栈结构中存在。如果存在交叉嵌套或自我引用,引擎会直接中断并抛出
TypeError: Converting circular structure to JSON。 - 类型特化与格式化输出:根据处理结果的最终类型进行字符串拼装,并根据
space参数插入换行与缩进空格。
三、深入 replacer 与 space:精准控制输出内容与外观
许多开发者仅使用单个参数,但 replacer 和 space 才是实现工程化数据过滤与排版的利器。
1. replacer 作为数组:属性白名单精确过滤
当 replacer 传入字符串或数字数组时,序列化过程将转变为严格的白名单提取模式。只有数组中声明的键名才会被保留到最终生成的 JSON 文本中,其余属性将被自动忽略。这在向客户端传输数据前过滤敏感字段(如密码、内部状态)时极为方便。
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,该键值对将从对象中剔除;如果返回其它值,该返回值将替换原先的属性值。
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 个字符)。常见做法是传入制表符
" "。
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"
}
四、特殊数据类型的转换规则与静默陷阱
由于 JSON 是一种独立于语言的文本协议,其语法仅原生支持 string、number、boolean、null、object 和 array 六类结构。当 JavaScript 特有的数据类型进入 JSON.stringify 时,会触发规范规定的降级或忽略策略。如果对其行为不了解,极易出现数据静默丢失的隐蔽 Bug。
1. undefined、函数与 Symbol 的三大宿主表现
这三种类型在 JSON 中均无对应表示,引擎处理它们遵循严格的宿主上下文判定:
- 作为对象的属性值时:该属性会被直接静默忽略,不出现在生成的键值对中。
- 作为数组的元素时:为了保持数组索引下标的连续性与完整性,它们会被统一转换为
null。 - 作为单独值被转换时:直接返回 JavaScript 原始值
undefined,而不是字符串"undefined"。
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。
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()转为数组或对象。
五、致命报错场景与安全解决对策
通常情况下 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 将其剪除。
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。
const payload = { txId: 9007199254740993n };
JSON.stringify(payload);
// 抛出: Uncaught TypeError: Do not know how to serialize a BigInt
解决策略 A:在 replacer 中做类型转字符串降级(推荐,不污染全局)
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(全局生效,谨慎使用)
// 仅在明确整个项目均接受大整数按字符串序列化时使用
BigInt.prototype.toJSON = function() {
return this.toString();
};
console.log(JSON.stringify({ id: 888888888888888888n }));
// 输出: {"id":"888888888888888888"}
六、自定义序列化:toJSON 方法的最佳实践
在面向对象或复杂领域模型开发中,通过在类中实现 toJSON 方法,可以赋予实例自主决定序列化数据的能力。
当一个对象拥有 toJSON() 方法时,JSON.stringify 会优先调用该方法,并用其返回的值作为该属性的实际序列化内容。执行顺序为:自身 toJSON() -> replacer 拦截器 -> 最终字符拼装。
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));
{"id":88,"name":"王五","cardMask":"6222****7890"}
七、工程实践:封装健壮的 safeStringify
在大型项目、微服务日志收集或第三方数据上报场景中,直接调用裸露的 JSON.stringify 是极不安全的。下面提供一个兼顾防循环引用崩溃、支持 BigInt 转换以及保证运行稳定的工业级封装函数:
/**
* 安全序列化函数:彻底解决循环引用、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;
}
}
| 关键检查维度 | 默认行为 | 推荐最佳实践 |
|---|---|---|
| 循环引用 (Circular Ref) | 抛出 TypeError 致命异常 | 使用 WeakSet 记录访问栈,成环节点替换为 "[Circular]" |
| 大整数 (BigInt) | 抛出 TypeError 致命异常 | 在 replacer 中显式转换为字符串 val.toString() |
| 缺失值 (undefined/函数/Symbol) | 对象属性静默丢失,数组元素转为 null | 涉及关键结构时,在数据层提供显式默认值或 null |
| 敏感信息过滤 | 全量可枚举属性输出 | 传递 replacer 数组白名单或使用领域模型内建 toJSON() |
| 网络传输 vs 本地调试 | 默认紧凑单行无空白 | 生产环境传输保持 space=null,本地调试或配置文件持久化使用 space=2 |
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01
为什么JS中typeof []返回object:数组类型检测误区解析
2026-11-04 19:50:47
相关标签:








![JS输出对象显示[object Object]怎么办?JSON.stringify格式化打印技巧](https://img.javascript.cn/tit/articles/13236/20261105/6eaec9baed78171fe0bc2b74184ba5b7b80ebd52de7d3323ff152268aa2df294.webp)