手写实现 JSON.stringify:处理日期、正则表达式、函数与循环引用的边缘情况

各位同仁,欢迎来到今天的技术研讨会。我们今天要探讨的主题是:深入理解并手写实现 JSON.stringify,特别是要聚焦于处理日期、正则表达式、函数以及最为棘手的循环引用等边缘情况。JSON.stringify 是 JavaScript 中一个看似简单却功能强大的工具,它将 JavaScript 值转换为 JSON 字符串。然而,在它的简洁背后,隐藏着一套精妙而严格的序列化规则,尤其是在处理各种复杂数据类型时,其行为往往出人意料,或者说,是高度符合 JSON 规范的。

作为一名编程专家,我们不仅仅要会使用工具,更要理解工具的内部工作原理。手写实现 JSON.stringify 不仅能加深我们对 JavaScript 类型系统、递归算法、内存管理以及 JSON 规范的理解,还能帮助我们在面对特定序列化需求时,能够设计出更健壮、更高效的自定义解决方案。

今天的讲座,我们将从 JSON.stringify 的核心行为入手,逐步构建我们的实现,并在每一步中,详细剖析其在各种边缘情况下的表现,以及我们如何在自己的代码中复现这些行为。

JSON 序列化的核心原则与原生行为

在深入实现之前,让我们先回顾一下 JSON.stringify 的原生行为,这将作为我们实现的目标和参照。

JSON.stringify 接收三个可选参数:

  1. value:要转换的 JavaScript 值。
  2. replacer:一个函数或数组,用于过滤或转换结果。
  3. space:一个字符串或数字,用于美化输出,添加缩进。

其核心序列化规则如下:

数据类型 JSON.stringify 的行为 备注
String 返回 JSON 字符串(带双引号,特殊字符转义)
Number 返回 JSON 数字。Infinity, NaN, null 数字表示为 null
Boolean 返回 truefalse
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。它还包含了对 undefinedFunctionSymbolBigInt 以及 toJSON 方法的初步处理,并引入了 seen 集合来检测循环引用。

边缘情况一:日期(Date 对象)

原生 JSON.stringifyDate 对象的处理方式非常明确:它会调用 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)、undefinedSymbol

这三种类型在 JSON.stringify 中有非常特殊的处理规则,它们不会被序列化为 JSON 字符串,而是根据其所在位置有不同的表现。

核心规则:

  • 顶层值: 如果 value 本身是 undefinedFunctionSymbolJSON.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 在遇到 undefinedFunctionSymbol 时返回 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() 方法的查找效率高。

算法步骤:

  1. 初始化 seen 集合:myStringify 函数的顶层,创建一个空的 Set,例如 const seen = new Set();
  2. 进入对象/数组时记录: 每当 serializeValue 函数接收到一个类型为 object 且非 null 的值时(包括数组和普通对象),在处理其内部属性/元素之前,先检查它是否已经在 seen 集合中。
    • 如果已经存在,说明这是一个循环引用,立即抛出 TypeError
    • 如果不存在,则将其添加到 seen 集合中。
  3. 退出对象/数组时移除: 当一个对象或数组的所有属性/元素都已处理完毕,即将返回其序列化字符串时,将其从 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 是一个函数时,它会为每个属性(包括数组元素和顶层值)调用。它接收两个参数:keyvaluethis 上下文指向拥有当前属性的对象。

  • 如果 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 + '}';
        // ...
    }
    // ...
}

通过巧妙地构造 currentGapnextIndent,我们可以在每个新层级开始时插入换行符和相应的缩进,从而实现美化输出。

最终的 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.stringifyreplacer 函数的 this 上下文,我们不能简单地将 this 绑定到 nullreplacer 函数的 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 的模拟有所简化,但对于 keyvalue 参数的传递是正确的。在实际生产级实现中,会更加精确。

测试用例与验证

让我们用一些测试用例来验证我们的 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 函数在处理各种基本类型、复杂结构以及日期、正则表达式、函数、循环引用、replacerspace 参数时,都能够符合或接近原生 JSON.stringify 的行为。

总结与展望

手写实现 JSON.stringify 是一次深刻的学习旅程。它不仅仅是关于如何将 JavaScript 数据类型转换为 JSON 字符串,更是对 JavaScript 核心类型系统、递归算法、内存管理(通过循环引用检测)以及 JSON 规范细节的一次全面检验。

我们从基础类型处理开始,逐步解决了日期格式化、正则表达式的特殊行为、函数和 undefined/Symbol 值的上下文敏感处理,以及最为复杂的循环引用检测。同时,我们也实现了 replacerspace 这两个高级参数,使得我们的序列化器功能更加完善。

尽管我们的实现可能在性能和一些极端边缘情况(例如 BigInt 的自定义处理、replacer this 上下文的精确模拟)上与原生实现存在差距,但这并不影响我们通过这个过程获得的宝贵知识和经验。理解这些内部机制,能够帮助我们更好地利用 JSON.stringify,并在需要时,设计出符合特定业务需求的自定义序列化方案。这是一个将理论知识转化为实践能力的绝佳范例。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注