JS对象转字符串时怎么处理函数、undefined和循环引用

作者:半夏

时间:2026-11-04

浏览:127

深入解析在JavaScript中将对象转为字符串时,原生JSON.stringify对函数、undefined和循环引用的默认行为与底层限制,并提供基于Replacer函数、WeakSet以及路径追踪的安全序列化与反序列化完整解决方案。

文章目录

在 JavaScript 开发中,使用 JSON.stringify() 将对象序列化为字符串是最常见的操作。然而原生 JSON 规范仅支持布尔值、数字、字符串、数组、对象及 null,并不包含 函数 与 undefined,更无法容纳数据图中的 循环引用。当直接序列化包含这些特殊属性的对象时,原生方法会静默丢失键值、将值异化为 null,甚至直接抛出 TypeError: Converting circular structure to JSON 异常导致进程中断。解决这一问题的标准路径在于:利用 JSON.stringify() 的第二个参数 Replacer 拦截属性转化,结合 WeakSet 追踪已访问对象的内存地址以解开环引用,必要时通过自定义递归序列化器实现保留与还原。

JS对象转字符串时怎么处理函数、undefined和循环引用
图 1:对象转字符串时特殊类型的处理流程概览

一、原生 JSON.stringify 的默认行为与缺陷

在不加任何定制的情况下,JavaScript 原生的 JSON.stringify() 遇到函数、undefined 和循环引用时,会采取非常不一致甚至破坏性的处理策略。理解这些默认表现是针对性处理的前提。

1. 函数与 undefined 在对象与数组中的异化

根据 ECMAScript 规范,JSON 格式要求每个键必须是双引号包裹的字符串,且值必须是有效的 JSON 节点。由于 JSON 不支持函数与 undefined,原生序列化引擎做了如下妥协:

  • 作为对象属性的值:整个键值对会被直接静默丢弃,不会出现在输出字符串中。
  • 作为数组的元素:为了保持数组的索引顺序一致性,它们不会被删除,而是被转换为 null。
  • 单独传入非对象:如果直接对一个函数或 undefined 执行序列化,则返回 undefined(注意这里是 JavaScript 的 undefined 原语,而不是字符串 "undefined")。
const sample = {
  a: "hello",
  b: undefined,
  c: function() { return 1; },
  list: [1, undefined, function() {}, "end"]
};

console.log(JSON.stringify(sample));
// 实际输出:
// {"a":"hello","list":[1,null,null,"end"]}
// 可以看到:属性 b 和 c 彻底消失,数组中的对应项退化为 null

2. 循环引用的致命抛错

当一个对象的属性直接或间接指向自身时(如 obj.self = obj),就形成了图结构中的有向环。原生的序列化器采用递归的方式深入每一个子属性。在遇到环形引用时,引擎无法判断何时终止遍历,为了防止调用栈溢出(Stack Overflow),V8 等引擎会立即抛出异常并终止脚本执行:

const user = { name: "Alice" };
user.profile = { owner: user }; // 循环引用

JSON.stringify(user);
// 抛出运行时错误: Uncaught TypeError: Converting circular structure to JSON
JS对象转字符串时怎么处理函数、undefined和循环引用
图 2:原生 JSON.stringify 对函数、undefined 和循环引用的处理特征对照

二、处理函数与 undefined:使用 Replacer 函数定制

JSON.stringify 拥有第二个可选参数:Replacer。它可以是一个函数或一个数组。当传入函数 (key, value) => modifiedValue 时,序列化器在遍历每一个属性之前都会先调用该函数。利用这一特性,我们可以将函数与 undefined 转换为 JSON 能够合法表示的形式,例如字符串标记或元数据对象。

Replacer 函数的执行规则
在序列化刚开始时,Replacer 第一次接收到的参数为 key: "",value: 当前待序列化的根对象。随后才会逐层递归子属性。因此在 Replacer 内部必须确保正确处理根节点。

1. 将函数与 undefined 转换为占位符或字符串

最直接的做法是将函数转化为其源码字符串(通过 fn.toString()),而将 undefined 转化为特定的字符串标记(如 "__UNDEFINED__")。

function replacer(key, value) {
  // 1. 保留函数逻辑:将函数转为文本内容
  if (typeof value === "function") {
    return `[Function: ${value.name || "anonymous"}]`;
    // 若后续需要还原执行,可以使用: return `__FUNCTION_START__${value.toString()}__FUNCTION_END__`;
  }
  
  // 2. 避免 undefined 被忽略:使用字符串标记占位
  if (value === undefined) {
    return "__UNDEFINED__";
  }

  return value;
}

const data = {
  title: "学习手册",
  author: undefined,
  calc: function(a, b) { return a + b; }
};

const jsonString = JSON.stringify(data, replacer, 2);
console.log(jsonString);

执行后的字符串输出如下:

{
  "title": "学习手册",
  "author": "__UNDEFINED__",
  "calc": "[Function: calc]"
}

原有的 author 键成功保留,而 calc 也被以描述性文本存入 JSON,不再被静默丢弃。

JS对象转字符串时怎么处理函数、undefined和循环引用
图 3:基于 Replacer 函数的值拦截与转换管道

三、处理循环引用:WeakSet 访问追踪机制

要解决 Converting circular structure to JSON 异常,核心思路是打破环路。在遍历属性树时,记录每一个已经访问过的复杂对象引用。当准备遍历某个子对象时,先检查它是否已在记录表中:如果已经存在,说明形成了环,立即用一个安全的标记(如 "[Circular]")替代它,而不再深入递归其内部。

1. 为什么优先使用 WeakSet 而不是 Set 或 Array?

  • 内存管理(避免泄漏):WeakSet 对其存储的对象持有的是“弱引用”。在大型复杂对象序列化完成后,记录表中的对象引用不会阻止垃圾回收机制(GC)正常回收内存。
  • 查询性能(O(1) 复杂度):基于哈希表实现的集合比数组的 indexOf / includes 具有更优秀的查询效率。
  • 类型自适应:WeakSet 只接受对象作为成员,这恰好契合循环引用的本质——只有对象和数组才会产生引用环,基础标量值无需检测。

2. 实现防循环引用的 Replacer

下面是一个标准的环检测 Replacer 实现:

function getCircularReplacer() {
  const seen = new WeakSet();
  return function(key, value) {
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        // 检测到环形引用,用安全字符串代替,截断无限递归
        return "[Circular]";
      }
      seen.add(value);
    }
    return value;
  };
}

// 验证用例
const nodeA = { name: "节点 A" };
const nodeB = { name: "节点 B" };
nodeA.sibling = nodeB;
nodeB.sibling = nodeA; // 形成相互循环引用

const safeResult = JSON.stringify(nodeA, getCircularReplacer(), 2);
console.log(safeResult);

输出结果为结构清晰且安全的 JSON 文本:

{
  "name": "节点 A",
  "sibling": {
    "name": "节点 B",
    "sibling": "[Circular]"
  }
}
JS对象转字符串时怎么处理函数、undefined和循环引用
图 4:使用 WeakSet 判定与截断循环引用的执行模型

四、综合实现:健壮的自定义序列化工具

将上述对函数、undefined 和循环引用的处理逻辑融为一体,我们可以构建一个高可靠性的自定义序列化函数 safeStringify。它同时兼顾了格式缩进、类型标记以及异常熔断。

/**
 * 安全的 JSON 序列化函数
 * 支持保留函数、保留 undefined、打标循环引用
 * @param {any} target - 待序列化的数据
 * @param {number|string} [space=2] - 缩进空格数
 * @returns {string} 序列化后的安全字符串
 */
function safeStringify(target, space = 2) {
  const seen = new WeakSet();

  return JSON.stringify(target, function(key, value) {
    // 1. 处理循环引用(仅针对非空对象)
    if (typeof value === "object" && value !== null) {
      if (seen.has(value)) {
        return "[Circular]";
      }
      seen.add(value);
      return value;
    }

    // 2. 处理函数:转换为带有标示的字符串
    if (typeof value === "function") {
      return `[Function: ${value.name || "anonymous"}]`;
    }

    // 3. 处理 undefined:转换为占位符,防止被忽略
    if (value === undefined) {
      return "__UNDEFINED__";
    }

    // 4. 处理 BigInt(原生 JSON 同样会报错)
    if (typeof value === "bigint") {
      return `${value.toString()}n`;
    }

    // 5. 处理 Symbol
    if (typeof value === "symbol") {
      return value.toString();
    }

    return value;
  }, space);
}

// 综合实测
const complexObj = {
  appName: "CoreApp",
  runTask: function execute() { return true; },
  config: undefined,
  meta: [1, undefined, function inline() {}]
};
complexObj.loop = complexObj; // 构造循环引用

console.log(safeStringify(complexObj));

运行结果完全符合预期:

{
  "appName": "CoreApp",
  "runTask": "[Function: execute]",
  "config": "__UNDEFINED__",
  "meta": [
    1,
    "__UNDEFINED__",
    "[Function: inline]"
  ],
  "loop": "[Circular]"
}

五、反序列化与安全性警示

如果对象转字符串仅仅是为了日志记录(如写入 Winston / Pino 日志文件)或调试打印,序列化为 [Function] 和 [Circular] 已经完全满足需求。但若目标是将字符串传输到远端并还原对象,需要格外注意以下两点:

安全警告:切勿盲目使用 eval 或 new Function
如果将函数序列化为 fn.toString() 并试图通过 eval("(" + fnStr + ")") 进行反序列化还原,将引发严重的代码注入(Remote Code Execution)安全漏洞。任何来自不可信输入源的恶意代码都将在当前环境中完整执行。在生产系统中,严禁直接动态执行外部反序列化生成的函数代码。

对于 undefined 的逆向还原,可以通过 JSON.parse 的第二个参数 Reviver 安全进行:

function safeParse(jsonString) {
  return JSON.parse(jsonString, function(key, value) {
    if (value === "__UNDEFINED__") {
      return undefined;
    }
    return value;
  });
}

六、常见场景与选型决策

在实际生产中,根据序列化的具体目的选择不同的方案,可以避免过度封装带来的性能开销:

业务场景 推荐方案 函数处理 undefined 处理 循环引用处理 能否完整反序列化
日志打印 / 报错排查 自定义 Replacer(如 safeStringify) 转描述字符串 转占位符 标记 [Circular] 只读展示,无需还原
复杂状态图快照 开源库 flatted 或 cycle.js 不推荐保留 支持 无损路径指针 ($0, $1) 完全支持还原
进程间通信 / 深拷贝 原生 structuredClone() 抛出 DOMException 原生完整支持 原生保留拓扑环 原生支持深克隆
JS对象转字符串时怎么处理函数、undefined和循环引用
图 5:JS 对象特殊类型序列化的技术选型决策图

七、总结

处理 JavaScript 对象转字符串时的函数、undefined 和循环引用,关键在于打破原生 JSON.stringify 的规范限制:

  • 对函数:通过 Replacer 将其转为描述文本 [Function: name] 或源码字符串,避免键值对静默丢失。
  • 对 undefined:通过统一标记(如 __UNDEFINED__)固化为字符串,防止对象属性被移除或数组索引被改写。
  • 对循环引用:基于 WeakSet 在运行时追踪复杂对象引用,一旦命中重复项立即替换为 [Circular],彻底阻止深度优先遍历导致的栈溢出崩溃。

在工程实践中,建议将这些转换规则统一封装入项目的基础工具库(如 safeStringify),在兼顾日志可观测性的同时,为系统运行筑牢稳定性底线。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章