各位同仁,各位编程爱好者,大家好!
今天,我们将深入探讨一个在前端和后端开发中都不可或缺的核心功能:JSON 序列化。具体来说,我们将手把手地实现一个功能完备的 JSON.stringify 函数,不仅覆盖其基本行为,还将详细处理 Symbol、undefined、Date 等特殊对象类型,并引入至关重要的循环引用检测机制。
理解并实现 JSON.stringify 不仅仅是为了重复造轮子,更重要的是,它能帮助我们深刻理解 JavaScript 数据类型与 JSON 格式之间的映射规则,掌握递归、状态管理和错误处理等高级编程技巧。在某些特定场景下,例如需要高度定制序列化行为、在特定环境中优化性能(尽管通常原生实现已足够优化)或进行深入的语言特性学习时,这种手写实现的能力将变得尤为宝贵。
1. JSON 与 JSON.stringify:基石与目的
1.1 什么是 JSON?
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它基于 JavaScript 编程语言的一个子集,但独立于任何编程语言。JSON 易于人阅读和编写,也易于机器解析和生成。它的核心数据结构只有两种:
- 键值对的集合:在 JavaScript 中表现为对象(
{ })。 - 值的有序列表:在 JavaScript 中表现为数组(
[ ])。
JSON 支持以下几种数据类型:
- 字符串 (String):双引号包围的 Unicode 字符序列,使用反斜杠进行转义。
- 数字 (Number):整数或浮点数。
- 布尔值 (Boolean):
true或false。 - 空值 (Null):
null。 - 对象 (Object):由键值对组成,键必须是字符串。
- 数组 (Array):值的有序集合。
请注意,JSON 不支持 undefined、function、Symbol、Date 等 JavaScript 特有的数据类型,也不支持 Infinity、NaN。这些类型在序列化时有其特定的转换规则。
1.2 JSON.stringify 的作用
JSON.stringify() 方法将一个 JavaScript 值(对象或数组)转换为 JSON 字符串。这是将 JavaScript 数据发送到服务器或存储到文件中的标准方式。它的基本语法是:
JSON.stringify(value, replacer, space)
value:要转换的 JavaScript 值。replacer(可选):一个函数或数组,用于过滤或转换结果。space(可选):一个字符串或数字,用于在输出 JSON 字符串中插入空白,提高可读性。
我们本次实现的目标,就是完整模拟这个原生方法的行为,包括对 replacer 和 space 参数的支持。
2. 核心序列化逻辑:处理基本数据类型与结构
要实现 JSON.stringify,我们首先需要一个能够递归处理不同数据类型的主函数。这个函数将根据输入值的类型,调用相应的序列化逻辑。
function myStringify(value, replacer, space) {
// 循环引用检测的Set,在最外层初始化
const visitedObjects = new Set();
// 用于处理缩进的函数
let gap = '';
if (typeof space === 'number' && space > 0) {
gap = ' '.repeat(Math.min(10, space));
} else if (typeof space === 'string') {
gap = space.slice(0, 10);
}
// 内部递归函数
function serialize(currentKey, currentValue, indentLevel) {
// 1. 处理replacer函数
let processedValue = currentValue;
if (typeof replacer === 'function') {
processedValue = replacer.call({ '': currentValue }, currentKey, currentValue);
}
// 2. 顶层 undefined, function, Symbol 返回 undefined
if (indentLevel === 0 && (processedValue === undefined || typeof processedValue === 'function' || typeof processedValue === 'symbol')) {
return undefined;
}
// 3. 处理基本类型
if (processedValue === null) {
return 'null';
}
if (typeof processedValue === 'boolean') {
return String(processedValue);
}
if (typeof processedValue === 'number') {
// NaN, Infinity, -Infinity 转换为 null
return Number.isFinite(processedValue) ? String(processedValue) : 'null';
}
if (typeof processedValue === 'string') {
return escapeJsonString(processedValue);
}
// BigInt 类型会抛出 TypeError
if (typeof processedValue === 'bigint') {
throw new TypeError('Do not know how to serialize a BigInt');
}
// 4. 处理 Date 对象 (具有 toJSON 方法)
if (processedValue instanceof Date) {
// Date 对象默认有 toJSON 方法,返回 ISO 格式字符串
return escapeJsonString(processedValue.toISOString());
}
// 5. 处理具有 toJSON 方法的对象
// toJSON 方法的优先级在循环引用检测之前
if (typeof processedValue.toJSON === 'function') {
return serialize(currentKey, processedValue.toJSON.call(processedValue), indentLevel);
}
// 6. 循环引用检测
if (typeof processedValue === 'object') {
if (visitedObjects.has(processedValue)) {
throw new TypeError('Converting circular structure to JSON');
}
visitedObjects.add(processedValue);
}
// 7. 处理数组
if (Array.isArray(processedValue)) {
const currentIndent = gap ? 'n' + gap.repeat(indentLevel + 1) : '';
const closingIndent = gap ? 'n' + gap.repeat(indentLevel) : '';
const elements = [];
for (let i = 0; i < processedValue.length; i++) {
// 数组中的 undefined, function, Symbol 转换为 null
let item = processedValue[i];
if (typeof replacer === 'function') {
item = replacer.call(processedValue, String(i), item);
}
if (item === undefined || typeof item === 'function' || typeof item === 'symbol') {
elements.push('null');
} else {
elements.push(serialize(String(i), item, indentLevel + 1));
}
}
// 移除当前对象,防止其被误判为循环引用,但对于数组,我们通常不将其视为循环引用的“节点”本身,
// 而是其元素。然而,visitedObjects 用于检测对象和数组本身是否被重复访问。
// 理论上,对于数组,我们也可以在处理完其元素后移除,但为了简化,我们让它一直留在visitedObjects中,
// 直到最外层函数结束。实际上,只要不出现 self-referencing array (如 arr[0]=arr),这个策略是安全的。
// 真正需要移除的是处理完对象属性后。
// For now, let's keep the `visitedObjects.delete(processedValue)` logic specific to objects.
const serializedElements = elements.filter(e => e !== undefined); // replacer返回undefined时会过滤
return '[' + currentIndent + serializedElements.join(',' + currentIndent) + closingIndent + ']';
}
// 8. 处理对象
if (typeof processedValue === 'object') {
const currentIndent = gap ? 'n' + gap.repeat(indentLevel + 1) : '';
const closingIndent = gap ? 'n' + gap.repeat(indentLevel) : '';
const properties = [];
let keysToProcess;
if (Array.isArray(replacer)) {
// 如果replacer是数组,只序列化数组中指定的属性
keysToProcess = replacer;
} else {
// 否则,获取所有可枚举的自有属性键
keysToProcess = Object.keys(processedValue);
}
for (const key of keysToProcess) {
// 确保键是字符串
const propertyKey = String(key);
let propertyValue = Object.getOwnPropertyDescriptor(processedValue, propertyKey);
if (!propertyValue) continue; // 属性可能来自原型链,或者不存在
propertyValue = propertyValue.value;
if (typeof replacer === 'function') {
propertyValue = replacer.call(processedValue, propertyKey, propertyValue);
}
// 对象属性值中的 undefined, function, Symbol 会被忽略
if (propertyValue === undefined || typeof propertyValue === 'function' || typeof propertyValue === 'symbol') {
continue;
}
const serializedValue = serialize(propertyKey, propertyValue, indentLevel + 1);
// 如果序列化后的值为 undefined,说明该属性不应包含在JSON中(例如replacer返回undefined)
if (serializedValue !== undefined) {
properties.push(escapeJsonString(propertyKey) + ':' + (gap ? ' ' : '') + serializedValue);
}
}
// 重要:在对象处理完成后,从visitedObjects中移除当前对象
// 这样可以允许同一个对象在不同的分支中被引用,只要它不构成循环。
// 但是,对于严格的循环引用检测,一旦检测到循环,就应该抛出错误。
// visitedObjects.delete(processedValue); // 这个删除逻辑是用于避免某些特定情况下的假阳性,但对于标准JSON.stringify的循环引用检测,一旦加入就应该保持,因为它是为了检测“回到”已访问节点。
// 实际上,为了模拟标准行为,一旦检测到循环就抛出错误,visitedObjects无需在每次递归后删除。
// 只有当一个对象完全处理完毕,且我们希望它能在其他不相关路径中被再次序列化(但这不是JSON.stringify的行为),才会删除。
// 对于JSON.stringify,它检测的是“当前正在被序列化的路径上是否包含了它自身或其祖先”。
// 所以,visitedObjects应该在递归完成后清除,而不是在每个对象处理完后清除。
return '{' + currentIndent + properties.join(',' + currentIndent) + closingIndent + '}';
}
// 默认情况,如果一个值不属于以上任何一种类型,且不是对象/数组,
// 那么它可能就是replacer返回的某个特殊值,或者我们遗漏了某种类型。
// 对于标准JSON.stringify,除了上述类型,其他如 RegExp, Error 等都序列化为空对象 {}。
// let's add this rule explicitly for robust behavior.
// 注意:这里需要确保processedValue不是原始值,因为原始值已在前面处理。
if (typeof processedValue === 'object') { // 再次检查以捕获如 new String('abc') 等包装对象
// 如果是包装对象,如 new String('abc'), new Number(123), new Boolean(true)
if (processedValue instanceof String) return escapeJsonString(processedValue.valueOf());
if (processedValue instanceof Number) return Number.isFinite(processedValue.valueOf()) ? String(processedValue.valueOf()) : 'null';
if (processedValue instanceof Boolean) return String(processedValue.valueOf());
// 对于其他未明确处理的对象类型,如 RegExp, Error, Map, Set 等,JSON.stringify 默认会将其序列化为空对象 {}
// 除非它们有 toJSON 方法。
// 这里的逻辑已经覆盖了 toJSON,所以如果到这里,说明没有 toJSON。
// 因此,这些对象将作为普通对象处理,但由于它们没有可枚举的自有属性,最终会变成 {}。
// 这是一个隐式行为,我们不需要特别写 `return '{}'` 除非我们想提前截断。
// 但为了精确模拟,我们让它走对象处理流程,自然得到 {}。
// 如果processedValue是类似`new Error()`或`new RegExp()`的实例,且没有toJSON方法,
// 且它们的可枚举属性为空,那么它们自然会序列化为`{}`。
// 故无需额外处理。
}
// 如果走到这里,说明replacer返回了一个我们没有预料到的类型,或者逻辑有缺陷。
// 为了安全,可以返回 undefined 或抛出错误,但通常不应该到达这里。
return undefined;
}
// 调用内部序列化函数,初始键为 "",初始缩进级别为 0
let result = serialize("", value, 0);
// 在最外层调用结束后,清空 visitedObjects
// 确保下次调用 myStringify 时,状态是全新的。
// visitedObjects.clear(); // 实际上,因为 visitedObjects 是在 myStringify 每次调用时新建的,所以不需要手动 clear。
return result;
}
2.1 字符串转义函数
JSON 字符串需要特殊的转义规则。例如,双引号 " 需要转义为 ",反斜杠 需要转义为 \,换行符 n 等。
function escapeJsonString(str) {
// 确保输入是字符串
if (typeof str !== 'string') {
return JSON.stringify(str); // 或者抛出错误,这里为了简化直接用原生
}
// JSON 字符串的转义规则
return '"' + str.replace(/[\"u0000-u001fu007f-u009fu00adu0600-u0604u070fu17b4u17b5u200c-u200fu2028-u202fu2060-u206fufeffufff0-uffff]/g, function (a) {
switch (a) {
case '\': return '\\';
case '"': return '\"';
case 'n': return '\n';
case 'r': return '\r';
case 't': return '\t';
case 'f': return '\f';
case 'b': return '\b';
default:
// 对于其他控制字符或特殊Unicode字符,转换为 uXXXX 形式
const hex = a.charCodeAt(0).toString(16);
return '\u' + '0000'.substring(hex.length) + hex;
}
}) + '"';
}
3. 特殊数据类型与 toJSON() 方法
3.1 undefined, function, Symbol 的特殊处理
这些类型在 JSON 中没有直接对应的表示。JSON.stringify 对它们的处理方式取决于它们出现的位置:
- 作为对象的值:属性会被完全忽略。
- 作为数组的元素:会被序列化为
null。 - 作为顶层值:整个
JSON.stringify调用会返回undefined。
// 在 serialize 函数内部的相应位置:
// 顶层 undefined, function, Symbol 返回 undefined
if (indentLevel === 0 && (processedValue === undefined || typeof processedValue === 'function' || typeof processedValue === 'symbol')) {
return undefined;
}
// 数组中的 undefined, function, Symbol 转换为 null
// ... (在处理数组元素时)
if (item === undefined || typeof item === 'function' || typeof item === 'symbol') {
elements.push('null');
} else {
elements.push(serialize(String(i), item, indentLevel + 1));
}
// 对象属性值中的 undefined, function, Symbol 会被忽略
// ... (在处理对象属性时)
if (propertyValue === undefined || typeof propertyValue === 'function' || typeof propertyValue === 'symbol') {
continue; // 跳过此属性
}
3.2 Date 对象的处理
Date 对象在 JSON 中被序列化为 ISO 8601 格式的字符串。这是因为 Date 对象本身有一个 toJSON 方法,JSON.stringify 会自动调用它。
// 在 serialize 函数内部的相应位置:
// 4. 处理 Date 对象 (具有 toJSON 方法)
if (processedValue instanceof Date) {
return escapeJsonString(processedValue.toISOString());
}
3.3 toJSON() 方法的优先级
JSON.stringify 在序列化一个对象时,如果该对象或其原型链上存在一个名为 toJSON 的方法,并且这个方法是一个函数,那么 JSON.stringify 会首先调用这个 toJSON 方法,并将它的返回值作为最终要序列化的值。这个行为优先级很高,甚至在循环引用检测之前。
// 在 serialize 函数内部的相应位置:
// 5. 处理具有 toJSON 方法的对象
// toJSON 方法的优先级在循环引用检测之前
if (processedValue !== null && typeof processedValue === 'object' && typeof processedValue.toJSON === 'function') {
// 调用 toJSON 方法,并用其返回值继续序列化过程
// 注意:toJSON 方法的 this 指向原始对象
return serialize(currentKey, processedValue.toJSON.call(processedValue), indentLevel);
}
3.4 BigInt 类型的处理
BigInt 是 JavaScript 中相对较新的原始类型。JSON.stringify 不支持 BigInt 类型,如果尝试序列化包含 BigInt 的对象,会抛出 TypeError。
// 在 serialize 函数内部的相应位置:
// BigInt 类型会抛出 TypeError
if (typeof processedValue === 'bigint') {
throw new TypeError('Do not know how to serialize a BigInt');
}
4. 循环引用检测:避免无限递归
循环引用是序列化过程中一个非常常见且危险的问题。如果一个对象直接或间接引用了自身,会导致无限递归,最终耗尽栈空间并抛出错误。JSON.stringify 通过检测循环引用来避免这种情况,并在检测到时抛出 TypeError。
4.1 问题与解决方案
问题:
const obj1 = {};
const obj2 = { a: obj1 };
obj1.b = obj2; // obj1 引用 obj2, obj2 引用 obj1,形成循环
JSON.stringify(obj1); // Throws TypeError: Converting circular structure to JSON
解决方案:
我们需要一个机制来追踪在当前序列化路径上已经访问过的所有对象。最常用的方法是使用一个 Set 来存储这些对象的引用。
- 每当开始处理一个对象(或数组,因为数组也可以包含循环引用)时,先检查它是否已经在
visitedObjects集合中。- 如果在,说明存在循环引用,抛出
TypeError。 - 如果不在,将其添加到
visitedObjects集合中。
- 如果在,说明存在循环引用,抛出
- 在递归调用
serialize函数时,将visitedObjects集合向下传递。
4.2 实现细节
visitedObjects 集合应该在最外层的 myStringify 函数调用时初始化一次,并贯穿整个递归过程。
// 在 myStringify 函数的开头定义:
function myStringify(value, replacer, space) {
const visitedObjects = new Set(); // 在最外层初始化
// ... 其他初始化代码 ...
function serialize(currentKey, currentValue, indentLevel) {
// ... 前面的基本类型和 toJSON 处理 ...
// 6. 循环引用检测
// 只有当 processedValue 是对象(非null)时才进行检测
if (typeof processedValue === 'object' && processedValue !== null) {
if (visitedObjects.has(processedValue)) {
throw new TypeError('Converting circular structure to JSON');
}
visitedObjects.add(processedValue);
}
// ... 数组和对象处理逻辑 ...
// 重要:循环引用检测的 Set 不应该在每次对象处理完后删除元素。
// 因为一个对象可能在序列化路径的深处再次被引用。
// Set 的生命周期应该与最外层的 myStringify 调用同步。
// 如果处理完毕,我们不再需要这个 Set,它会在下一次 myStringify 调用时重新创建。
// 但如果需要模拟 replacer 函数在不同上下文中复用同一个 visited 状态,则需要更复杂的管理。
// 对于模拟标准行为,当前设计是正确的。
}
// ... 调用 serialize 并返回结果 ...
}
为什么不删除 visitedObjects 中的元素?
考虑以下结构:A -> B -> C -> B。
当序列化 A 时,我们访问 B,将其加入 visitedObjects。然后访问 C,将其加入。当从 C 访问 B 时,B 已经在 visitedObjects 中,此时我们检测到循环并抛出错误。
如果我们在处理完 B 的所有属性后将其从 visitedObjects 中删除,那么 C 再次引用 B 时,B 将不会被视为循环引用,这与 JSON.stringify 的行为不符。JSON.stringify 检测的是当前正在序列化的路径上是否存在循环。
5. replacer 和 space 参数
JSON.stringify 提供了两个可选参数,允许我们定制序列化行为。
5.1 replacer 参数
replacer 可以是一个函数或一个数组。
作为函数:
replacer 函数在每个键值对被序列化之前调用。它接收两个参数:key 和 value。
this上下文指向拥有key的对象。- 如果
replacer函数返回undefined,则该属性不会包含在 JSON 字符串中。 - 如果返回其他值,该值将作为
value进行序列化。 - 对于数组元素,
key是元素的索引(字符串形式)。 - 对于顶层值,
key是一个空字符串""。
作为数组:
replacer 数组包含字符串或数字,用于指定哪些属性应该包含在 JSON 字符串中。只有这些属性会被序列化。
// 在 serialize 函数内部的相应位置:
function serialize(currentKey, currentValue, indentLevel) {
// 1. 处理replacer函数
let processedValue = currentValue;
// replacer函数在所有值上都会被调用,包括原始值和顶层值。
// 但是,原生JSON.stringify在顶层值(非对象/数组)上调用replacer时,
// key是空字符串,value是原始值。如果replacer返回undefined,则会直接返回undefined。
// 如果返回其他值,则该值会被序列化。
// 对于对象/数组内部,replacer的行为是在处理当前键值对时调用。
// 确保replacer是函数且不是顶层undefined/function/symbol,
// 因为对于这些类型,replacer的返回值可能会影响顶层行为。
// 这里的processedValue是经过toJSON处理后的值
if (typeof replacer === 'function') {
// replacer的this上下文是拥有该属性的对象。
// 对于顶层值,拥有它的对象是一个虚拟的 { "": value }。
processedValue = replacer.call(this, currentKey, processedValue); // this上下文需要传入
}
// ... 后续逻辑使用 processedValue ...
}
// 在 myStringify 函数中调用 serialize 时,需要传递正确的 this 上下文。
// 对于顶层调用,this 上下文是 { "": value }。
// 为了简化,我们可以在 serialize 函数内部模拟这个行为,而不是在调用时传递。
// 修正:replacer.call(this, currentKey, processedValue)
// 对于顶层调用,this 应该是一个包装对象 `{ '': value }`。
// 对于对象属性,this 是该对象本身。对于数组元素,this 是该数组本身。
// 我们可以修改 serialize 函数的签名,使其接收一个 parentObject 参数来设置 this。
// 改进后的 serialize 签名和调用:
function myStringify(value, replacer, space) {
const visitedObjects = new Set();
let gap = '';
// ... gap 初始化 ...
// 内部递归函数
// parentObject:当前值所属的父对象或父数组。对于顶层值,是一个虚拟对象 { "": value }。
function serialize(parentObject, currentKey, currentValue, indentLevel) {
// 1. 处理replacer函数
let processedValue = currentValue;
if (typeof replacer === 'function') {
processedValue = replacer.call(parentObject, currentKey, currentValue);
}
// ... 后续逻辑使用 processedValue ...
// 对象处理时:
// ... (在遍历对象属性时)
// propertyValue = replacer.call(processedValue, propertyKey, propertyValue); // 这里processedValue是当前对象,正确
// 数组处理时:
// ... (在遍历数组元素时)
// item = replacer.call(processedValue, String(i), item); // 这里processedValue是当前数组,正确
}
// 初始调用
let result = serialize({ "": value }, "", value, 0); // 顶层调用的 parentObject 是 { "": value }
return result;
}
// 针对 replacer 作为数组的修改:
// 在 serialize 函数的对象处理部分
// ...
if (typeof processedValue === 'object') {
// ...
let keysToProcess;
if (Array.isArray(replacer)) {
// 如果replacer是数组,只序列化数组中指定的属性
keysToProcess = replacer;
} else {
// 否则,获取所有可枚举的自有属性键
keysToProcess = Object.keys(processedValue);
}
for (const key of keysToProcess) {
// ...
// 调用 serialize 时,parentObject 传入 processedValue (当前对象)
const serializedValue = serialize(processedValue, propertyKey, propertyValue, indentLevel + 1);
// ...
}
// ...
}
replacer 优先级:
replacer 函数在 toJSON 方法之后,循环引用检测之前执行。这是因为 toJSON 的返回值才是 replacer 真正要处理的 value。
// 在 serialize 函数内部的顺序:
// 1. 调用 toJSON 方法 (如果存在)
// 2. 调用 replacer 函数 (如果存在)
// 3. 执行循环引用检测
// 4. 根据类型进行序列化
对 serialize 函数签名的修正与 replacer 调用时机
为了严格模拟 JSON.stringify 的 replacer 行为,我们需要注意 this 上下文和 key 的传递。replacer 接收 key 和 value,其中 key 是当前属性名(或数组索引),value 是当前属性值。this 指向包含 value 的对象。
// 重新组织 serialize 函数的内部逻辑,更清晰地处理 replacer
function myStringify(value, replacer, space) {
const visitedObjects = new Set();
let gap = '';
if (typeof space === 'number' && space > 0) {
gap = ' '.repeat(Math.min(10, space));
} else if (typeof space === 'string') {
gap = space.slice(0, 10);
}
// 内部递归函数
// currentParent: 当前值所属的父对象或父数组。对于顶层值,是一个虚拟的 `{ "" : value }`。
// currentKey: 当前值的键名 (字符串形式)。对于顶层值,是 `""`。
// currentValue: 待序列化的值。
// indentLevel: 当前的缩进级别。
function serialize(currentParent, currentKey, currentValue, indentLevel) {
// 1. 处理 toJSON 方法
let valueToProcess = currentValue;
if (valueToProcess !== null && typeof valueToProcess === 'object' && typeof valueToProcess.toJSON === 'function') {
valueToProcess = valueToProcess.toJSON.call(valueToProcess);
}
// 2. 处理 replacer 函数
// replacer 的 this 上下文是 currentParent,key 是 currentKey
if (typeof replacer === 'function') {
valueToProcess = replacer.call(currentParent, currentKey, valueToProcess);
}
// 3. 顶层 undefined, function, Symbol 返回 undefined
// 注意:这里是处理 replacer 返回后的值
if (indentLevel === 0 && (valueToProcess === undefined || typeof valueToProcess === 'function' || typeof valueToProcess === 'symbol')) {
return undefined;
}
// 4. 处理基本类型
if (valueToProcess === null) {
return 'null';
}
if (typeof valueToProcess === 'boolean') {
return String(valueToProcess);
}
if (typeof valueToProcess === 'number') {
return Number.isFinite(valueToProcess) ? String(valueToProcess) : 'null';
}
if (typeof valueToProcess === 'string') {
return escapeJsonString(valueToProcess);
}
if (typeof valueToProcess === 'bigint') {
throw new TypeError('Do not know how to serialize a BigInt');
}
// 5. 循环引用检测
// 只对对象进行循环引用检测 (包括数组,因为数组也是对象)
if (typeof valueToProcess === 'object') {
if (visitedObjects.has(valueToProcess)) {
throw new TypeError('Converting circular structure to JSON');
}
visitedObjects.add(valueToProcess);
}
// 6. 处理数组
if (Array.isArray(valueToProcess)) {
const currentIndent = gap ? 'n' + gap.repeat(indentLevel + 1) : '';
const closingIndent = gap ? 'n' + gap.repeat(indentLevel) : '';
const elements = [];
for (let i = 0; i < valueToProcess.length; i++) {
// 数组元素递归调用 serialize
// 注意:这里传递的 currentParent 是 valueToProcess (当前数组)
const serializedElement = serialize(valueToProcess, String(i), valueToProcess[i], indentLevel + 1);
// 数组中的 undefined, function, Symbol 转换为 null
if (serializedElement === undefined || typeof serializedElement === 'function' || typeof serializedElement === 'symbol' || serializedElement === null) {
elements.push('null');
} else {
elements.push(serializedElement);
}
}
// 循环引用检测后,移除当前数组。
// 这一步对于严格模拟标准行为很重要,因为一个数组可以被多个地方引用,只要不形成循环。
// 但如果数组元素本身就是数组,且形成循环,则会在内部检测到。
visitedObjects.delete(valueToProcess); // 允许同一数组在不同路径中被引用
return '[' + currentIndent + elements.join(',' + currentIndent) + closingIndent + ']';
}
// 7. 处理对象
if (typeof valueToProcess === 'object') {
const currentIndent = gap ? 'n' + gap.repeat(indentLevel + 1) : '';
const closingIndent = gap ? 'n' + gap.repeat(indentLevel) : '';
const properties = [];
let keysToProcess;
if (Array.isArray(replacer)) {
// 如果replacer是数组,只序列化数组中指定的属性
keysToProcess = replacer;
} else {
// 否则,获取所有可枚举的自有属性键
keysToProcess = Object.keys(valueToProcess);
}
for (const key of keysToProcess) {
// 确保键是字符串
const propertyKey = String(key);
const descriptor = Object.getOwnPropertyDescriptor(valueToProcess, propertyKey);
// 过滤掉不可枚举的属性或不存在的属性 (replacer数组可能包含不存在的键)
if (!descriptor || !descriptor.enumerable) {
continue;
}
let propertyValue = descriptor.value;
// 对象属性值中的 undefined, function, Symbol 会被忽略
// 注意:这里调用 serialize 时,传入的 currentParent 是 valueToProcess (当前对象)
const serializedValue = serialize(valueToProcess, propertyKey, propertyValue, indentLevel + 1);
if (serializedValue !== undefined) { // 如果 serialize 返回 undefined,则跳过此属性
properties.push(escapeJsonString(propertyKey) + ':' + (gap ? ' ' : '') + serializedValue);
}
}
// 循环引用检测后,移除当前对象。
visitedObjects.delete(valueToProcess); // 允许同一对象在不同路径中被引用
return '{' + currentIndent + properties.join(',' + currentIndent) + closingIndent + '}';
}
// 对于其他未明确处理的对象类型 (如 RegExp, Error, Map, Set 等),
// 它们若没有 toJSON 方法,且不是包装对象,则默认序列化为 `{}`
// 但由于它们的可枚举属性通常为空,最终也会得到 `{}`,所以无需额外处理。
// 对于包装对象 (new String, new Number, new Boolean),在基本类型处理阶段已处理。
return undefined; // 最终捕获,如果走到这里,说明replacer返回了不合法的值
}
// 初始调用 serialize,顶层 parent 是一个虚拟对象,key 是空字符串
const initialParent = { "": value };
let result = serialize(initialParent, "", value, 0);
return result;
}
5.2 space 参数
space 参数用于控制输出 JSON 字符串的格式化和缩进,提高可读性。
- 数字:如果
space是一个数字(0-10),则表示每个缩进级别应使用多少个空格。 - 字符串:如果
space是一个字符串,则将其作为缩进字符,最多使用前 10 个字符。
// 在 myStringify 函数的开头初始化 gap:
let gap = '';
if (typeof space === 'number' && space > 0) {
gap = ' '.repeat(Math.min(10, space));
} else if (typeof space === 'string') {
gap = space.slice(0, 10);
}
// 在 serialize 函数内部使用 gap 来生成缩进:
// 对于数组和对象,生成当前缩进和下一级缩进。
const currentIndent = gap ? 'n' + gap.repeat(indentLevel + 1) : '';
const closingIndent = gap ? 'n' + gap.repeat(indentLevel) : '';
// 拼接时加入缩进和换行符
// 例如:
// return '[' + currentIndent + elements.join(',' + currentIndent) + closingIndent + ']';
// return '{' + currentIndent + properties.join(',' + currentIndent) + closingIndent + '}';
6. 完整代码示例
将上述所有逻辑整合到一个 myStringify 函数中。
function escapeJsonString(str) {
if (typeof str !== 'string') {
// Fallback for non-string types, though theoretically `typeof str` should be 'string' here
// if called correctly by myStringify.
return JSON.stringify(str);
}
return '"' + str.replace(/[\"u0000-u001fu007f-u009fu00adu0600-u0604u070fu17b4u17b5u200c-u200fu2028-u202fu2060-u206fufeffufff0-uffff]/g, function (a) {
switch (a) {
case '\': return '\\';
case '"': return '\"';
case 'n': return '\n';
case 'r': return '\r';
case 't': return '\t';
case 'f': return '\f';
case 'b': return '\b';
default:
const hex = a.charCodeAt(0).toString(16);
return '\u' + '0000'.substring(hex.length) + hex;
}
}) + '"';
}
function myStringify(value, replacer, space) {
const visitedObjects = new Set(); // 用于循环引用检测
let gap = ''; // 缩进字符串
if (typeof space === 'number' && space > 0) {
gap = ' '.repeat(Math.min(10, space));
} else if (typeof space === 'string') {
gap = space.slice(0, 10);
}
/**
* 内部递归序列化函数
* @param {Object|Array} currentParent 当前值所属的父对象或父数组。对于顶层值,是一个虚拟的 `{ "" : value }`。
* @param {string} currentKey 当前值的键名 (字符串形式)。对于顶层值,是 `""`。
* @param {*} currentValue 待序列化的值。
* @param {number} indentLevel 当前的缩进级别。
* @returns {string|undefined} 序列化后的 JSON 字符串片段,或 `undefined` 表示该值应被忽略。
*/
function serialize(currentParent, currentKey, currentValue, indentLevel) {
// 1. 处理 toJSON 方法:如果存在,先调用 toJSON 并用其返回值进行后续处理
let valueToProcess = currentValue;
if (valueToProcess !== null && typeof valueToProcess === 'object' && typeof valueToProcess.toJSON === 'function') {
// toJSON 方法的 this 上下文是它所属的对象
valueToProcess = valueToProcess.toJSON.call(valueToProcess);
}
// 2. 处理 replacer 函数:如果存在,调用 replacer 并用其返回值进行后续处理
// replacer 的 this 上下文是 currentParent,key 是 currentKey
if (typeof replacer === 'function') {
valueToProcess = replacer.call(currentParent, currentKey, valueToProcess);
}
// 3. 处理顶层值:顶层 undefined, function, Symbol 返回 undefined
// 注意:这里是处理 replacer 返回后的值
if (indentLevel === 0 && (valueToProcess === undefined || typeof valueToProcess === 'function' || typeof valueToProcess === 'symbol')) {
return undefined;
}
// 4. 处理基本类型
if (valueToProcess === null) {
return 'null';
}
if (typeof valueToProcess === 'boolean') {
return String(valueToProcess);
}
if (typeof valueToProcess === 'number') {
// NaN, Infinity, -Infinity 转换为 null
return Number.isFinite(valueToProcess) ? String(valueToProcess) : 'null';
}
if (typeof valueToProcess === 'string') {
return escapeJsonString(valueToProcess);
}
if (typeof valueToProcess === 'bigint') {
// BigInt 类型会抛出 TypeError
throw new TypeError('Do not know how to serialize a BigInt');
}
// 5. 处理包装对象 (String, Number, Boolean)
// 这些对象会被解包成其原始值进行序列化
if (valueToProcess instanceof String) return escapeJsonString(valueToProcess.valueOf());
if (valueToProcess instanceof Number) return Number.isFinite(valueToProcess.valueOf()) ? String(valueToProcess.valueOf()) : 'null';
if (valueToProcess instanceof Boolean) return String(valueToProcess.valueOf());
// 6. 循环引用检测
// 只对对象(包括数组)进行检测,因为基本类型不会有循环引用
if (typeof valueToProcess === 'object') {
if (visitedObjects.has(valueToProcess)) {
// 检测到循环引用,抛出 TypeError
throw new TypeError('Converting circular structure to JSON');
}
// 将当前对象添加到已访问集合
visitedObjects.add(valueToProcess);
}
// 7. 处理数组
if (Array.isArray(valueToProcess)) {
const nextIndentLevel = indentLevel + 1;
const currentIndentStr = gap ? 'n' + gap.repeat(nextIndentLevel) : '';
const closingIndentStr = gap ? 'n' + gap.repeat(indentLevel) : '';
const elements = [];
for (let i = 0; i < valueToProcess.length; i++) {
// 数组元素递归调用 serialize
// 注意:这里传递的 currentParent 是 valueToProcess (当前数组)
const serializedElement = serialize(valueToProcess, String(i), valueToProcess[i], nextIndentLevel);
// 数组中的 undefined, function, Symbol 转换为 null
if (serializedElement === undefined || typeof serializedElement === 'function' || typeof serializedElement === 'symbol') {
elements.push('null');
} else {
elements.push(serializedElement);
}
}
// 数组处理完成后,从已访问集合中移除当前数组,允许它在不同路径中被再次引用
// 如果一个数组是自引用的 (arr[0] = arr),它会在 visitedObjects.has(valueToProcess) 处被检测到。
visitedObjects.delete(valueToProcess);
return '[' + currentIndentStr + elements.join(',' + currentIndentStr) + closingIndentStr + ']';
}
// 8. 处理对象
if (typeof valueToProcess === 'object') {
const nextIndentLevel = indentLevel + 1;
const currentIndentStr = gap ? 'n' + gap.repeat(nextIndentLevel) : '';
const closingIndentStr = gap ? 'n' + gap.repeat(indentLevel) : '';
const properties = [];
let keysToProcess;
if (Array.isArray(replacer)) {
// 如果 replacer 是数组,只序列化数组中指定的属性
keysToProcess = replacer;
} else {
// 否则,获取所有可枚举的自有属性键
keysToProcess = Object.keys(valueToProcess);
}
for (const key of keysToProcess) {
const propertyKey = String(key); // 确保键是字符串
const descriptor = Object.getOwnPropertyDescriptor(valueToProcess, propertyKey);
// 过滤掉不可枚举的属性或不存在的属性 (replacer 数组可能包含不存在的键)
if (!descriptor || !descriptor.enumerable) {
continue;
}
let propertyValue = descriptor.value;
// 对象属性值递归调用 serialize
// 注意:这里传递的 currentParent 是 valueToProcess (当前对象)
const serializedValue = serialize(valueToProcess, propertyKey, propertyValue, nextIndentLevel);
// 如果序列化后的值为 undefined,则跳过此属性(例如 replacer 返回 undefined)
// 或者属性值是 function/Symbol,它们也会被 serialize 内部逻辑处理为 undefined
if (serializedValue !== undefined) {
properties.push(escapeJsonString(propertyKey) + ':' + (gap ? ' ' : '') + serializedValue);
}
}
// 对象处理完成后,从已访问集合中移除当前对象,允许它在不同路径中被再次引用
visitedObjects.delete(valueToProcess);
return '{' + currentIndentStr + properties.join(',' + currentIndentStr) + closingIndentStr + '}';
}
// 默认情况:如果一个值不属于以上任何一种类型,且不是对象/数组
// 例如,replacer 返回了一个我们不识别的类型,或者原始的 function/Symbol
// 在对象属性中,这些值会被忽略;在数组中,它们会被转换为 'null'。
// 但如果它们出现在这里,说明是顶层或者某种特殊情况,此时应该返回 undefined
return undefined;
}
// 初始调用 serialize,顶层 parent 是一个虚拟对象 { "": value },key 是空字符串
const initialParent = { "": value };
let result = serialize(initialParent, "", value, 0);
return result;
}
7. 边缘情况与比较
7.1 包装对象
new String('hello'), new Number(123), new Boolean(true) 这样的包装对象,JSON.stringify 会将其解包为原始值 hello, 123, true 进行序列化。我们的实现已经涵盖了这一点。
7.2 Map 和 Set
原生 JSON.stringify 对 Map 和 Set 对象的处理方式如下:
Map会被序列化为空对象{},除非它有toJSON方法。Set会被序列化为空对象{},除非它有toJSON方法。
我们的实现中,如果Map或Set没有toJSON方法,它们会作为普通对象进入处理流程。由于Map和Set的默认可枚举属性为空,最终它们也会被序列化为{},这与原生行为一致。
7.3 Symbol 作为键
Symbol 类型的键是不可枚举的,因此 Object.keys() 不会返回它们。JSON.stringify 也完全忽略 Symbol 键。我们的 Object.keys() 策略自然地实现了这一点。
7.4 性能考量
手写 JSON.stringify 主要是为了学习和特殊定制,在性能上通常无法与浏览器或 Node.js 环境中高度优化的原生 JSON.stringify 相媲美。原生实现通常由 C++ 或其他底层语言编写,经过了大量优化。
7.5 严格符合规范
JSON.stringify 的规范非常详细,涵盖了许多微妙的边缘情况。我们的实现尽可能地遵循了这些规范,但要做到 100% 严格符合所有角落案例,可能需要更复杂的逻辑,例如对代理对象(Proxy)的处理,或者对各种内置类型(如 WeakMap, WeakSet, ArrayBuffer, SharedArrayBuffer, DataView 等)的精确行为模拟。对于大多数实际应用场景,我们目前的实现已经足够健壮。
8. 总结与展望
通过这次深入的探讨与实现,我们不仅复刻了 JSON.stringify 的核心功能,还掌握了处理 Symbol、undefined、Date 等特殊类型以及至关重要的循环引用检测机制。这趟旅程不仅加深了我们对 JavaScript 类型系统和 JSON 规范的理解,更锻炼了递归、状态管理和错误处理等编程能力。理解数据序列化的原理,是构建健壮应用、进行数据交换和持久化的基石。未来,我们可以在此基础上,探索更多定制化的序列化需求,例如特定格式的日期处理、自定义类的序列化方案,甚至实现更高效的二进制序列化协议。