各位同仁,欢迎来到今天的技术研讨会。我们今天要探讨的主题是:深入理解并手写实现 JSON.stringify,特别是要聚焦于处理日期、正则表达式、函数以及最为棘手的循环引用等边缘情况。JSON.stringify 是 JavaScript 中一个看似简单却功能强大的工具,它将 JavaScript 值转换为 JSON 字符串。然而,在它的简洁背后,隐藏着一套精妙而严格的序列化规则,尤其是在处理各种复杂数据类型时,其行为往往出人意料,或者说,是高度符合 JSON 规范的。
作为一名编程专家,我们不仅仅要会使用工具,更要理解工具的内部工作原理。手写实现 JSON.stringify 不仅能加深我们对 JavaScript 类型系统、递归算法、内存管理以及 JSON 规范的理解,还能帮助我们在面对特定序列化需求时,能够设计出更健壮、更高效的自定义解决方案。
今天的讲座,我们将从 JSON.stringify 的核心行为入手,逐步构建我们的实现,并在每一步中,详细剖析其在各种边缘情况下的表现,以及我们如何在自己的代码中复现这些行为。
JSON 序列化的核心原则与原生行为
在深入实现之前,让我们先回顾一下 JSON.stringify 的原生行为,这将作为我们实现的目标和参照。
JSON.stringify 接收三个可选参数:
value:要转换的 JavaScript 值。replacer:一个函数或数组,用于过滤或转换结果。space:一个字符串或数字,用于美化输出,添加缩进。
其核心序列化规则如下:
| 数据类型 | JSON.stringify 的行为 |
备注 |
|---|---|---|
String |
返回 JSON 字符串(带双引号,特殊字符转义) | |
Number |
返回 JSON 数字。Infinity, NaN, null 数字表示为 null |
|
Boolean |
返回 true 或 false。 |
|
null |
返回 null。 |
|
Array |
递归序列化每个元素,以 [] 包裹,元素间用 , 分隔。 |
undefined, Function, Symbol 值在数组中会被转换为 null。 |
Object |
递归序列化可枚举的自有属性,以 {} 包裹,属性间用 , 分隔。 |
键必须是字符串。undefined, Function, Symbol 值在对象中会被跳过。 |
Date |
返回 ISO 8601 格式的字符串(例如:"2023-10-26T10:00:00.000Z")。 | toJSON 方法优先。 |
RegExp |
返回 {}(空对象)。 |
|
Function |
undefined。如果在对象中作为值,属性会被跳过。在数组中,会被转换为 null。 |
|
Symbol |
undefined。行为同 Function。 |
|
undefined |
undefined。行为同 Function。 |
如果是顶级值,返回 undefined。 |
BigInt |
抛出 TypeError。 |
unless toJSON or replacer handles it. |
Map/Set |
返回 {}(空对象)。 |
它们是对象,但内部数据不可枚举。 |
| 循环引用 | 抛出 TypeError。 |
"Converting circular structure to JSON"。 |
具有 toJSON() 方法的对象 |
调用其 toJSON() 方法,并序列化其返回值。 |
理解了这些规则,我们就可以开始构建我们的 JSON.stringify 函数。
构建基础序列化器
首先,我们从最基本的类型处理开始。我们将创建一个主函数 myStringify,并在内部定义一个递归辅助函数 serializeValue,用于处理不同类型的值。
function myStringify(value, replacer, space) {
// 跟踪循环引用
const seen = 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 serializeValue(val, indent) {
const currentGap = indent ? 'n' + indent : '';
const nextIndent = indent + gap;
// 1. 处理基本类型和特殊值
if (val === null) {
return 'null';
}
const type = typeof val;
if (type === 'string') {
// 字符串需要转义
return JSON.stringify(val); // 借用原生转义能力
}
if (type === 'number') {
if (Number.isFinite(val)) {
return String(val);
}
return 'null'; // NaN, Infinity 序列化为 null
}
if (type === 'boolean') {
return String(val);
}
// 2. 处理 undefined, Function, Symbol, BigInt
// 顶层值为 undefined, Function, Symbol 时,返回 undefined
// BigInt 顶层会抛出错误
if (val === undefined || type === 'function' || type === 'symbol') {
// 如果是顶层调用,则直接返回 undefined
// 在对象或数组中,它们会被特殊处理,由调用者决定是跳过还是转为 null
return undefined;
}
if (type === 'bigint') {
// BigInt 默认抛出 TypeError
throw new TypeError('Do not know how to serialize a BigInt');
}
// 3. 处理具有 toJSON 方法的对象
// Date 对象是典型的例子
if (val && typeof val.toJSON === 'function') {
// toJSON 方法可以返回任何类型的值,然后这个返回值会被递归序列化
return serializeValue(val.toJSON(), indent);
}
// 4. 处理循环引用
if (typeof val === 'object') {
if (seen.has(val)) {
throw new TypeError('Converting circular structure to JSON');
}
seen.add(val);
}
// 5. 处理数组
if (Array.isArray(val)) {
if (val.length === 0) {
seen.delete(val); // 数组处理完毕,从 seen 中移除
return '[]';
}
const elements = [];
for (let i = 0; i < val.length; i++) {
const elementValue = serializeValue(val[i], nextIndent);
// 数组中的 undefined, Function, Symbol 会被序列化为 null
elements.push(elementValue === undefined ? 'null' : elementValue);
}
seen.delete(val); // 数组处理完毕,从 seen 中移除
return '[' + currentGap + nextIndent + elements.join(',' + currentGap + nextIndent) + currentGap + indent + ']';
}
// 6. 处理普通对象
if (typeof val === 'object') {
const keys = Object.keys(val); // 只考虑可枚举的自有属性
if (keys.length === 0) {
seen.delete(val); // 对象处理完毕,从 seen 中移除
return '{}';
}
const properties = [];
for (const key of keys) {
const keyValue = val[key];
const serializedKeyValue = serializeValue(keyValue, nextIndent);
// 对象属性值为 undefined, Function, Symbol 时,整个属性会被跳过
if (serializedKeyValue !== undefined) {
properties.push(JSON.stringify(key) + ':' + (gap ? ' ' : '') + serializedKeyValue);
}
}
seen.delete(val); // 对象处理完毕,从 seen 中移除
return '{' + currentGap + nextIndent + properties.join(',' + currentGap + nextIndent) + currentGap + indent + '}';
}
// 理论上不会走到这里,除非有我们未考虑到的新类型
return undefined;
}
// 顶层值处理
let result = serializeValue(value, '');
// replacer 函数处理
if (typeof replacer === 'function') {
// replacer 函数会以一个临时的空对象作为根,将要序列化的值作为其唯一的属性值
// key 为空字符串
const wrapper = { '': value };
result = serializeValue(replacer.call(wrapper, '', value), '');
} else if (Array.isArray(replacer)) {
// replacer 数组处理:只保留数组中指定的属性
// 这一部分会影响对象序列化的 keys 选取
// 我们需要修改 serializeValue 中的对象处理逻辑
// 为了简化,我们暂时将replacer数组的逻辑留给后续更高级的实现
// 暂时先让它等同于没有replacer
// (在实际的myStringify中,replacer数组通常在对象遍历时过滤key)
}
// 最终返回结果
if (result === undefined) {
return undefined;
}
// 顶级 undefined, function, symbol 会返回 undefined
// 其他如 NaN, Infinity 顶层会返回 "null"
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
return undefined;
}
return result;
}
这段代码初步构建了一个能够处理基本类型、数组和普通对象的 myStringify。它还包含了对 undefined、Function、Symbol、BigInt 以及 toJSON 方法的初步处理,并引入了 seen 集合来检测循环引用。
边缘情况一:日期(Date 对象)
原生 JSON.stringify 对 Date 对象的处理方式非常明确:它会调用 Date 实例的 toJSON() 方法,该方法返回一个 ISO 8601 格式的字符串。
原生行为示例:
const date = new Date('2023-10-26T14:30:00.000Z');
console.log(JSON.stringify(date)); // "2023-10-26T14:30:00.000Z"
我们的 serializeValue 函数已经包含了对 toJSON 方法的通用处理。由于 Date 对象天然就有一个 toJSON 方法,所以我们的实现会自动遵循这个规则。
在 serializeValue 中的体现:
// 3. 处理具有 toJSON 方法的对象
if (val && typeof val.toJSON === 'function') {
// Date 对象会走到这里,调用其 toJSON 方法返回 ISO 字符串
// 然后这个字符串会被递归序列化(即直接返回带引号的字符串)
return serializeValue(val.toJSON(), indent);
}
这意味着,如果 val 是一个 Date 对象,val.toJSON() 会被调用,返回一个 ISO 格式的字符串。然后这个字符串会再次进入 serializeValue,并作为普通的字符串被处理,最终得到带双引号的 JSON 字符串。这与原生 JSON.stringify 的行为完全一致。
边缘情况二:正则表达式(RegExp 对象)
RegExp 对象在 JSON.stringify 中的行为可能令人有些意外。它不会像 Date 对象那样被转换为一个有意义的字符串,而是简单地被序列化为空对象 {}。
原生行为示例:
const regex = /abc/gi;
console.log(JSON.stringify(regex)); // {}
实现方法:
在我们的 serializeValue 函数中,RegExp 对象也是 typeof val === 'object' 的一个实例。它没有 toJSON 方法,也不是数组。因此,它会进入到普通对象的处理逻辑。由于 RegExp 对象的自有可枚举属性通常为空(或者说,其内部状态不是通过可枚举属性暴露的),所以它最终会被序列化为空对象。
在 serializeValue 中的体现:
// ... (省略之前的处理)
// 6. 处理普通对象 (RegExp 会走到这里)
if (typeof val === 'object') {
const keys = Object.keys(val); // 对于 RegExp 对象,keys 数组通常是空的
if (keys.length === 0) {
seen.delete(val); // 对象处理完毕,从 seen 中移除
return '{}'; // 因此 RegExp 最终会返回 '{}'
}
// ... (省略对象属性遍历逻辑)
}
这个行为也是符合原生 JSON.stringify 规范的。如果我们需要自定义 RegExp 的序列化方式(例如,将其转换为字符串 /abc/gi),则需要在其原型链上添加 toJSON 方法,或者通过 replacer 函数进行处理。但遵循原生行为,我们不需要额外特殊处理 RegExp。
边缘情况三:函数(Function)、undefined 和 Symbol
这三种类型在 JSON.stringify 中有非常特殊的处理规则,它们不会被序列化为 JSON 字符串,而是根据其所在位置有不同的表现。
核心规则:
- 顶层值: 如果
value本身是undefined、Function或Symbol,JSON.stringify返回undefined。 - 数组元素: 在数组中,它们会被转换为
null。 - 对象属性值: 在对象中,包含这些值的属性会被完全跳过。
原生行为示例:
console.log(JSON.stringify(undefined)); // undefined
console.log(JSON.stringify(() => {})); // undefined
console.log(JSON.stringify(Symbol('foo'))); // undefined
console.log(JSON.stringify([1, undefined, 2, () => {}, Symbol('bar')]));
// "[1,null,2,null,null]"
const obj = {
a: 1,
b: undefined,
c: () => {},
d: Symbol('baz'),
e: 'hello'
};
console.log(JSON.stringify(obj));
// '{"a":1,"e":"hello"}'
实现方法:
我们的 serializeValue 函数已经包含了对这些类型的初步处理,但需要确保在不同上下文中的行为一致。
在 serializeValue 中的体现:
function serializeValue(val, indent) {
// ... (省略之前的处理)
// 顶层值为 undefined, Function, Symbol, BigInt 时,返回 undefined 或抛出错误
// 但这个判断是在递归内部,所以需要与顶层调用协同
if (val === undefined || typeof val === 'function' || typeof val === 'symbol') {
// 在数组或对象中,我们会返回一个特殊标记,让上层决定如何处理
// 例如,对于数组,我们返回 null;对于对象,我们返回 undefined (表示跳过)
// 这里返回 undefined,让外部逻辑判断
return undefined;
}
// ... (省略 toJSON 和循环引用处理)
// 5. 处理数组
if (Array.isArray(val)) {
// ...
const elements = [];
for (let i = 0; i < val.length; i++) {
const elementValue = serializeValue(val[i], nextIndent);
// 数组中的 undefined, Function, Symbol 会被序列化为 null
elements.push(elementValue === undefined ? 'null' : elementValue);
}
// ...
}
// 6. 处理普通对象
if (typeof val === 'object') {
// ...
const properties = [];
for (const key of keys) {
const keyValue = val[key];
const serializedKeyValue = serializeValue(keyValue, nextIndent);
// 对象属性值为 undefined, Function, Symbol 时,整个属性会被跳过
// 如果 serializeValue 返回 undefined,则跳过
if (serializedKeyValue !== undefined) {
properties.push(JSON.stringify(key) + ':' + (gap ? ' ' : '') + serializedKeyValue);
}
}
// ...
}
// ...
}
// 顶层调用 myStringify 时的特殊处理
// (这部分代码已经包含在 myStringify 函数的末尾)
// 最终返回结果
// if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
// return undefined;
// }
// return result;
这里的关键在于 serializeValue 在遇到 undefined、Function、Symbol 时返回 undefined。然后由调用它的上下文(数组循环或对象循环)来决定如何处理这个 undefined:
- 在数组中,
elementValue === undefined ? 'null' : elementValue会将其转换为'null'。 - 在对象中,
if (serializedKeyValue !== undefined)会导致该属性被跳过。
对于顶层值,我们已经在 myStringify 的末尾添加了判断,确保如果 value 本身是这些类型,则返回 undefined。
边缘情况四:循环引用
这是 JSON.stringify 实现中最具挑战性的一环。当一个对象或数组直接或间接引用自身时,就会形成循环引用。原生 JSON.stringify 在遇到这种情况时,会抛出一个 TypeError。
原生行为示例:
const obj1 = {};
const obj2 = { a: obj1 };
obj1.b = obj2; // obj1 -> obj2 -> obj1 形成循环
try {
JSON.stringify(obj1);
} catch (e) {
console.error(e.message); // Converting circular structure to JSON
}
实现方法:
为了检测循环引用,我们需要在递归遍历对象和数组时,跟踪所有已经访问过的对象引用。一个 Set 数据结构非常适合这个任务,因为它可以存储对象引用,并且 has() 方法的查找效率高。
算法步骤:
- 初始化
seen集合: 在myStringify函数的顶层,创建一个空的Set,例如const seen = new Set();。 - 进入对象/数组时记录: 每当
serializeValue函数接收到一个类型为object且非null的值时(包括数组和普通对象),在处理其内部属性/元素之前,先检查它是否已经在seen集合中。- 如果已经存在,说明这是一个循环引用,立即抛出
TypeError。 - 如果不存在,则将其添加到
seen集合中。
- 如果已经存在,说明这是一个循环引用,立即抛出
- 退出对象/数组时移除: 当一个对象或数组的所有属性/元素都已处理完毕,即将返回其序列化字符串时,将其从
seen集合中移除。这是为了避免不必要的内存占用,并确保在处理兄弟分支时不会错误地标记为循环引用(尽管对于 JSON.stringify 而言,一旦进入一个对象,其子属性的循环引用都会被捕获,移除主要为了清理)。
在 serializeValue 中的体现:
function serializeValue(val, indent) {
// ... (省略之前的处理)
// 4. 处理循环引用
// 只对对象和数组进行跟踪,基本类型不会形成循环引用
if (typeof val === 'object' && val !== null) { // 确保不是 null
if (seen.has(val)) {
throw new TypeError('Converting circular structure to JSON');
}
seen.add(val); // 记录当前对象
}
// 5. 处理数组
if (Array.isArray(val)) {
// ... (序列化逻辑)
seen.delete(val); // 数组处理完毕,从 seen 中移除
return '[' + currentGap + nextIndent + elements.join(',' + currentGap + nextIndent) + currentGap + indent + ']';
}
// 6. 处理普通对象
if (typeof val === 'object') {
// ... (序列化逻辑)
seen.delete(val); // 对象处理完毕,从 seen 中移除
return '{' + currentGap + nextIndent + properties.join(',' + currentGap + nextIndent) + currentGap + indent + '}';
}
// ...
}
通过这种 seen 集合的机制,我们可以有效地在递归过程中捕获并阻止循环引用,从而模拟原生 JSON.stringify 的行为。
replacer 参数的实现
replacer 参数是一个强大的工具,它允许我们在序列化过程中过滤或转换值。它可以是一个函数,也可以是一个字符串数组。
replacer 作为函数
当 replacer 是一个函数时,它会为每个属性(包括数组元素和顶层值)调用。它接收两个参数:key 和 value。this 上下文指向拥有当前属性的对象。
- 如果
replacer返回undefined,则该属性(如果是对象属性)会被跳过,或者该元素(如果是数组元素)会被转换为null。 - 如果
replacer返回其他值,则该返回值会被进一步序列化。
原生行为示例:
const myObject = {
a: 1,
b: 'hello',
c: new Date(),
d: function() {},
e: undefined
};
function replacerFunc(key, value) {
if (key === 'b') {
return undefined; // 跳过 'b' 属性
}
if (key === 'a') {
return value * 10; // 转换 'a' 的值
}
if (typeof value === 'function') {
return '[Function]'; // 转换函数为字符串
}
return value; // 默认返回原始值
}
console.log(JSON.stringify(myObject, replacerFunc, 2));
/*
{
"a": 10,
"c": "2023-10-26T14:30:00.000Z"
}
*/
实现方法:
我们需要修改 serializeValue 函数,使其在处理每个属性/元素之前,先调用 replacer 函数。
function myStringify(value, replacer, space) {
const seen = 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 serializeValue(key, val, indent) { // 增加 key 参数
const currentGap = indent ? 'n' + indent : '';
const nextIndent = indent + gap;
// --- replacer 函数的调用位置 ---
let processedValue = val;
let parentObject = null; // 用于设置 replacer 的 this 上下文,需要从上层传入
if (typeof replacer === 'function') {
// 对于顶层值,key 是空字符串
// 对于数组元素,key 是索引的字符串形式
// 对于对象属性,key 是属性名
// 这里我们暂时简化,不精确模拟 this 上下文,只关注 key/value
// 真实的 replacer 会在 value 被其父对象持有的时候才被调用
// 所以这里需要更精细的控制,我们可以在对象和数组的遍历中调用 replacer
// 而不是在这里提前调用
}
// 由于 replacer 会影响所有级别的序列化,最直接的方式是在对象和数组的遍历逻辑中调用它
// 对于顶层值,我们会在 myStringify 的末尾特殊处理一次
// ... (以下是之前的类型处理逻辑,对 val 进行操作)
// 为了 replacer 能够正确作用,我们需要将 replacer 的调用逻辑下放到
// 对象属性遍历和数组元素遍历的内部,以及顶层值的处理。
// 这是因为 replacer 的 `key` 参数是当前正在被序列化的属性的键名(或索引),
// 并且 `this` 应该指向包含该属性的对象。
// 重新组织 serializeValue 以支持 replacer
// 这是一个更精细的实现,将 replacer 的调用推迟到实际处理属性/元素时
// 并且需要一个包装函数来处理顶层值
if (val === null) return 'null';
if (typeof val.toJSON === 'function') {
val = val.toJSON();
}
if (typeof val === 'object' && val !== null) {
if (seen.has(val)) {
throw new TypeError('Converting circular structure to JSON');
}
seen.add(val);
}
// 基本类型处理
const type = typeof val;
if (type === 'string') return JSON.stringify(val);
if (type === 'number') return Number.isFinite(val) ? String(val) : 'null';
if (type === 'boolean') return String(val);
if (val === undefined || type === 'function' || type === 'symbol') return undefined; // 在这里仍返回 undefined,让上层决定如何处理
if (type === 'bigint') throw new TypeError('Do not know how to serialize a BigInt');
if (Array.isArray(val)) {
const elements = [];
for (let i = 0; i < val.length; i++) {
// 在这里调用 replacer
let elementVal = val[i];
if (typeof replacer === 'function') {
// this 应该是数组本身
elementVal = replacer.call(val, String(i), elementVal);
}
const serializedElement = serializeValue(String(i), elementVal, nextIndent); // 传递 key
elements.push(serializedElement === undefined ? 'null' : serializedElement);
}
seen.delete(val);
return '[' + currentGap + nextIndent + elements.join(',' + currentGap + nextIndent) + currentGap + indent + ']';
}
if (typeof val === 'object') {
let keys = Object.keys(val);
// 如果 replacer 是数组,则只保留指定的 key
if (Array.isArray(replacer)) {
keys = keys.filter(k => replacer.includes(k));
}
const properties = [];
for (const k of keys) {
let propertyVal = val[k];
if (typeof replacer === 'function') {
// this 应该是当前对象
propertyVal = replacer.call(val, k, propertyVal);
}
const serializedProperty = serializeValue(k, propertyVal, nextIndent); // 传递 key
if (serializedProperty !== undefined) {
properties.push(JSON.stringify(k) + ':' + (gap ? ' ' : '') + serializedProperty);
}
}
seen.delete(val);
return '{' + currentGap + nextIndent + properties.join(',' + currentGap + nextIndent) + currentGap + indent + '}';
}
return undefined; // Fallback
}
// 处理顶层值和 replacer 的调用
let initialValue = value;
if (typeof replacer === 'function') {
// 对于顶层值,key 是空字符串,this 应该指向一个包含该值的临时对象
initialValue = replacer.call({ '': value }, '', value);
}
// 最终序列化
const finalResult = serializeValue('', initialValue, ''); // 顶层 key 为空字符串
// 处理顶层 undefined, Function, Symbol
if (initialValue === undefined || typeof initialValue === 'function' || typeof initialValue === 'symbol') {
return undefined;
}
return finalResult;
}
这段重构后的代码将 serializeValue 中的 key 参数用于 replacer 函数的调用,并调整了 myStringify 顶层对 replacer 的处理。
replacer 作为数组
当 replacer 是一个字符串数组时,它充当一个白名单。只有数组中包含的属性名才会被序列化。这个规则只对对象属性生效,不影响数组元素。
原生行为示例:
const obj = {
a: 1,
b: 'hello',
c: true,
d: {
x: 10,
y: 20
}
};
console.log(JSON.stringify(obj, ['a', 'c', 'd']));
// '{"a":1,"c":true,"d":{"x":10,"y":20}}'
// 注意 'd' 内部的 'x', 'y' 仍然会被序列化,replacer 数组只作用于当前层级的属性
实现方法:
在 serializeValue 处理对象时,我们需要根据 replacer 数组来过滤 Object.keys(val) 的结果。
if (typeof val === 'object') {
let keys = Object.keys(val);
// 如果 replacer 是数组,则只保留指定的 key
if (Array.isArray(replacer)) {
keys = keys.filter(k => replacer.includes(k));
}
const properties = [];
for (const k of keys) {
// ... (后续逻辑不变)
}
// ...
}
这样就实现了 replacer 数组的功能。它有效地限制了哪些属性可以被序列化。
space 参数的实现
space 参数用于美化输出,使其更具可读性。它可以是一个数字或一个字符串。
- 数字: 如果
space是一个 0 到 10 之间的数字,它表示缩进的空格数量。 - 字符串: 如果
space是一个字符串,它表示用于缩进的字符序列,最多取前 10 个字符。
原生行为示例:
const data = { a: 1, b: [2, 3], c: { d: 4 } };
console.log(JSON.stringify(data)); // '{"a":1,"b":[2,3],"c":{"d":4}}'
console.log(JSON.stringify(data, null, 2));
/*
{
"a": 1,
"b": [
2,
3
],
"c": {
"d": 4
}
}
*/
console.log(JSON.stringify(data, null, 't'));
/*
{
"a": 1,
"b": [
2,
3
],
"c": {
"d": 4
}
}
*/
实现方法:
我们已经在 myStringify 的开头定义了 gap 变量来存储缩进字符串,并在 serializeValue 中通过 indent 参数递归传递当前的缩进级别。
function myStringify(value, replacer, space) {
// ...
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 serializeValue(key, val, indent) {
const currentGap = indent ? 'n' + indent : ''; // 当前层级的换行和缩进
const nextIndent = indent + gap; // 下一层级的缩进
// ...
// 在数组和对象的拼接中加入 currentGap 和 nextIndent
// return '[' + currentGap + nextIndent + elements.join(',' + currentGap + nextIndent) + currentGap + indent + ']';
// return '{' + currentGap + nextIndent + properties.join(',' + currentGap + nextIndent) + currentGap + indent + '}';
// ...
}
// ...
}
通过巧妙地构造 currentGap 和 nextIndent,我们可以在每个新层级开始时插入换行符和相应的缩进,从而实现美化输出。
最终的 myStringify 函数整合
现在,我们将所有讨论的逻辑整合到一个完整的 myStringify 函数中。
function myStringify(value, replacer, space) {
const seen = new Set(); // 用于检测循环引用
// 处理 space 参数,生成缩进字符串
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 {string} key 当前属性的键名(对于数组元素是索引的字符串形式,对于顶层值是空字符串)
* @param {*} val 要序列化的值
* @param {string} indent 当前层级的缩进字符串
* @returns {string|undefined} 序列化后的 JSON 字符串,或 undefined (表示跳过)
*/
function serializeValue(key, val, indent) {
// 应用 replacer 函数
let processedVal = val;
if (typeof replacer === 'function') {
// this 上下文是包含 val 的父对象,或者是一个临时对象(对于顶层 val)
// 注意:这里需要更精细的父对象传递,暂时简化为 null
// 实际上,replacer.call(parent, key, val)
// 为了模拟原生行为,replacer 的 this 应该是父对象
// 在这里我们无法直接获取父对象,所以 replacer 函数的 this 模拟是比较困难的。
// 实际实现中,通常会将父对象作为参数传递给 serializeValue。
// 为了简化并侧重于主要逻辑,我们暂时忽略 this 上下文的精确模拟,
// 仅关注 key 和 value 参数。
// 但如果严格遵循规范,应该在对象和数组遍历时直接调用 replacer.call(val, currentKey, currentValue)
processedVal = replacer.call(null, key, val); // 这里的 this 绑定到 null,与原生行为有差异
}
// 处理 toJSON 方法
if (processedVal && typeof processedVal.toJSON === 'function') {
// toJSON 方法可以返回任何类型的值,其返回值会再次进入序列化流程
processedVal = processedVal.toJSON();
}
// 处理基本类型
if (processedVal === null) {
return 'null';
}
const type = typeof processedVal;
if (type === 'string') {
return JSON.stringify(processedVal); // 借用原生转义能力
}
if (type === 'number') {
if (Number.isFinite(processedVal)) {
return String(processedVal);
}
return 'null'; // NaN, Infinity 序列化为 null
}
if (type === 'boolean') {
return String(processedVal);
}
// 处理 undefined, Function, Symbol, BigInt
// 这些类型在对象属性中会被跳过,在数组中会被转为 null
// 顶层值时返回 undefined
if (processedVal === undefined || type === 'function' || type === 'symbol') {
return undefined;
}
if (type === 'bigint') {
throw new TypeError('Do not know how to serialize a BigInt value');
}
// 处理循环引用 (只对对象和数组进行跟踪)
if (typeof processedVal === 'object') { // 此时 processedVal 不为 null
if (seen.has(processedVal)) {
throw new TypeError('Converting circular structure to JSON');
}
seen.add(processedVal); // 记录当前对象/数组
}
const currentLineBreak = indent ? 'n' : '';
const nextIndent = indent + gap;
const indentStr = gap ? nextIndent : '';
const endIndent = gap ? indent : '';
// 处理数组
if (Array.isArray(processedVal)) {
if (processedVal.length === 0) {
seen.delete(processedVal);
return '[]';
}
const elements = [];
for (let i = 0; i < processedVal.length; i++) {
// 再次调用 serializeValue,并传入数组索引作为 key
const elementResult = serializeValue(String(i), processedVal[i], nextIndent);
// 数组中的 undefined, Function, Symbol 会被转换为 null
elements.push(elementResult === undefined ? 'null' : elementResult);
}
seen.delete(processedVal); // 数组处理完毕,从 seen 中移除
return '[' + currentLineBreak + indentStr + elements.join(',' + currentLineBreak + indentStr) + currentLineBreak + endIndent + ']';
}
// 处理普通对象 (包括 RegExp, Map, Set 等,它们的可枚举属性通常为空)
if (typeof processedVal === 'object') {
let keys = Object.keys(processedVal); // 获取可枚举的自有属性键
// 如果 replacer 是数组,则根据白名单过滤键
if (Array.isArray(replacer)) {
keys = keys.filter(k => replacer.includes(k));
}
const properties = [];
for (const k of keys) {
// 再次调用 serializeValue,并传入属性名作为 key
const propertyResult = serializeValue(k, processedVal[k], nextIndent);
// 对象属性值为 undefined, Function, Symbol 时,整个属性会被跳过
if (propertyResult !== undefined) {
properties.push(JSON.stringify(k) + ':' + (gap ? ' ' : '') + propertyResult);
}
}
seen.delete(processedVal); // 对象处理完毕,从 seen 中移除
return '{' + currentLineBreak + indentStr + properties.join(',' + currentLineBreak + indentStr) + currentLineBreak + endIndent + '}';
}
// 理论上不应到达这里
return undefined;
}
// 针对顶层 value 的特殊处理
let finalValueToSerialize = value;
if (typeof replacer === 'function') {
// 顶层值调用 replacer 时,key 为空字符串,this 指向一个包含该值的临时对象
// 这里的 this 模拟仍然不够完美,但足以测试 replacer 的参数
finalValueToSerialize = replacer.call({ '': value }, '', value);
}
// 如果顶层值或 replacer 转换后的顶层值是 undefined, Function, Symbol,则返回 undefined
if (finalValueToSerialize === undefined || typeof finalValueToSerialize === 'function' || typeof finalValueToSerialize === 'symbol') {
return undefined;
}
// 开始序列化,顶层 key 为空字符串
return serializeValue('', finalValueToSerialize, '');
}
关于 replacer 函数的 this 上下文的修正说明:
在 serializeValue 中,为了严格模拟原生 JSON.stringify 中 replacer 函数的 this 上下文,我们不能简单地将 this 绑定到 null。replacer 函数的 this 应该指向包含当前属性的父对象。这意味着我们需要在 serializeValue 的调用链中传递父对象引用。
例如,在对象属性遍历时:
propertyResult = serializeValue(k, processedVal[k], nextIndent, processedVal);
在数组元素遍历时:
elementResult = serializeValue(String(i), processedVal[i], nextIndent, processedVal);
然后 serializeValue 的签名变为 function serializeValue(key, val, indent, parent),并在调用 replacer 时使用 replacer.call(parent, key, val);。对于顶层值,parent 可以是一个包装对象 { '': value }。
由于这会使代码复杂度显著增加,并且超出了一般手写实现对主要逻辑的关注,上述的 myStringify 版本对 this 的模拟有所简化,但对于 key 和 value 参数的传递是正确的。在实际生产级实现中,会更加精确。
测试用例与验证
让我们用一些测试用例来验证我们的 myStringify 函数。
console.log("--- Test Case 1: Basic Types ---");
console.log("myStringify(123):", myStringify(123)); // "123"
console.log("myStringify('hello'):", myStringify('hello')); // ""hello""
console.log("myStringify(true):", myStringify(true)); // "true"
console.log("myStringify(null):", myStringify(null)); // "null"
console.log("myStringify(undefined):", myStringify(undefined)); // undefined
console.log("myStringify(NaN):", myStringify(NaN)); // "null"
console.log("myStringify(Infinity):", myStringify(Infinity)); // "null"
console.log("myStringify(Symbol('test')):", myStringify(Symbol('test'))); // undefined
console.log("myStringify(() => {}):", myStringify(() => {})); // undefined
// console.log("myStringify(123n):", myStringify(123n)); // Throws TypeError
console.log("n--- Test Case 2: Arrays ---");
console.log("myStringify([1, 'a', true, null]):", myStringify([1, 'a', true, null])); // "[1,"a",true,null]"
console.log("myStringify([1, undefined, () => {}, Symbol('foo'), 2]):", myStringify([1, undefined, () => {}, Symbol('foo'), 2])); // "[1,null,null,null,2]"
console.log("myStringify([]):", myStringify([])); // "[]"
console.log("n--- Test Case 3: Objects ---");
const obj1 = { a: 1, b: 'b', c: true, d: null };
console.log("myStringify(obj1):", myStringify(obj1)); // "{"a":1,"b":"b","c":true,"d":null}"
const obj2 = { a: 1, b: undefined, c: () => {}, d: Symbol('bar'), e: new Date() };
console.log("myStringify(obj2):", myStringify(obj2)); // "{"a":1,"e":"2023-10-26T...Z"}" (undefined, func, symbol skipped)
console.log("myStringify({a: {b:1}}):", myStringify({a: {b:1}})); // "{"a":{"b":1}}"
console.log("myStringify({}):", myStringify({})); // "{}"
console.log("n--- Test Case 4: Dates ---");
const date = new Date('2023-10-26T14:30:00.000Z');
console.log("myStringify(date):", myStringify(date)); // ""2023-10-26T14:30:00.000Z""
console.log("n--- Test Case 5: RegExp ---");
const regex = /test/gi;
console.log("myStringify(regex):", myStringify(regex)); // "{}"
console.log("n--- Test Case 6: Circular References ---");
const circularObj = {};
const innerCircular = { x: 1 };
circularObj.self = circularObj; // Direct circular
circularObj.inner = innerCircular;
innerCircular.parent = circularObj; // Indirect circular
try {
myStringify(circularObj);
} catch (e) {
console.error("Circular error (direct):", e.message); // Converting circular structure to JSON
}
const arrCircular = [];
arrCircular[0] = arrCircular;
try {
myStringify(arrCircular);
} catch (e) {
console.error("Circular error (array):", e.message); // Converting circular structure to JSON
}
console.log("n--- Test Case 7: replacer Function ---");
const replacerObj = { a: 1, b: 'foo', c: new Date(), d: () => {} };
function replacerFn(key, value) {
if (key === 'b') return undefined; // Skip 'b'
if (typeof value === 'number') return value * 10; // Transform numbers
if (typeof value === 'function') return '[Function Placeholder]'; // Convert functions
return value;
}
console.log("myStringify(replacerObj, replacerFn):", myStringify(replacerObj, replacerFn));
// Expected: {"a":10,"c":"2023-10-26T...Z"} or similar based on date, function will be skipped unless explicitly handled by replacer.
// If replacerFn returns '[Function Placeholder]' for function, it should appear.
// The current replacer implementation returns value, so if replacerFn returns '[Function Placeholder]', it will be used.
// Output should be: {"a":10,"c":"2023-10-26T...Z","d":"[Function Placeholder]"}
console.log("n--- Test Case 8: replacer Array ---");
const replacerArrObj = { a: 1, b: 'foo', c: true, d: { x: 10 } };
console.log("myStringify(replacerArrObj, ['a', 'c', 'd']):", myStringify(replacerArrObj, ['a', 'c', 'd']));
// Expected: {"a":1,"c":true,"d":{"x":10}}
console.log("n--- Test Case 9: space Argument ---");
const spaceObj = { a: 1, b: [2, { c: 3 }], d: 'text' };
console.log("myStringify(spaceObj, null, 2):");
console.log(myStringify(spaceObj, null, 2));
/* Expected:
{
"a": 1,
"b": [
2,
{
"c": 3
}
],
"d": "text"
}
*/
console.log("myStringify(spaceObj, null, '\t'):");
console.log(myStringify(spaceObj, null, 't'));
/* Expected:
{
"a": 1,
"b": [
2,
{
"c": 3
}
],
"d": "text"
}
*/
console.log("n--- Test Case 10: toJSON method ---");
const customObj = {
value: 100,
toJSON: function() {
return { custom: this.value * 2 };
}
};
console.log("myStringify(customObj):", myStringify(customObj)); // "{"custom":200}"
const customString = {
toString: function() { return 'not used'; },
toJSON: function() { return 'custom string'; }
};
console.log("myStringify(customString):", myStringify(customString)); // ""custom string""
通过这些测试用例,我们可以看到我们的 myStringify 函数在处理各种基本类型、复杂结构以及日期、正则表达式、函数、循环引用、replacer 和 space 参数时,都能够符合或接近原生 JSON.stringify 的行为。
总结与展望
手写实现 JSON.stringify 是一次深刻的学习旅程。它不仅仅是关于如何将 JavaScript 数据类型转换为 JSON 字符串,更是对 JavaScript 核心类型系统、递归算法、内存管理(通过循环引用检测)以及 JSON 规范细节的一次全面检验。
我们从基础类型处理开始,逐步解决了日期格式化、正则表达式的特殊行为、函数和 undefined/Symbol 值的上下文敏感处理,以及最为复杂的循环引用检测。同时,我们也实现了 replacer 和 space 这两个高级参数,使得我们的序列化器功能更加完善。
尽管我们的实现可能在性能和一些极端边缘情况(例如 BigInt 的自定义处理、replacer this 上下文的精确模拟)上与原生实现存在差距,但这并不影响我们通过这个过程获得的宝贵知识和经验。理解这些内部机制,能够帮助我们更好地利用 JSON.stringify,并在需要时,设计出符合特定业务需求的自定义序列化方案。这是一个将理论知识转化为实践能力的绝佳范例。