跳到主要内容

实现简易 JSON.parse

问题

不使用 JSON.parse,手动实现一个函数解析 JSON 字符串。

示例:

myJsonParse('{"name":"hello","age":25,"active":true,"data":null}');
// { name: 'hello', age: 25, active: true, data: null }

myJsonParse('[1, "two", [3, 4], {"a": 5}]');
// [1, 'two', [3, 4], { a: 5 }]
为什么要手写?
  • 考察对 JSON 规范的理解
  • 考察递归下降解析器的基本思路(编译原理基础)
  • 考察字符串处理和边界情况
  • 实际场景:自定义 JSON 解析规则(如支持注释、BigInt)

答案

方法一:递归下降解析器(推荐)

核心思路:维护一个 pos 指针,从左到右逐个字符解析,根据当前字符判断 JSON 值的类型。

jsonParse.ts
function myJsonParse(json: string): any {
let pos = 0;

function parseValue(): any {
skipWhitespace();
const char = json[pos];

if (char === '"') return parseString();
if (char === '{') return parseObject();
if (char === '[') return parseArray();
if (char === 't' || char === 'f') return parseBoolean();
if (char === 'n') return parseNull();
// 数字以 - 或 0-9 开头
if (char === '-' || (char >= '0' && char <= '9')) return parseNumber();

throw new SyntaxError(`Unexpected character '${char}' at position ${pos}`);
}

function parseString(): string {
pos++; // 跳过开头的 "
let result = '';

while (pos < json.length) {
const char = json[pos];

if (char === '"') {
pos++; // 跳过结尾的 "
return result;
}

if (char === '\\') {
pos++; // 跳过反斜杠
const escaped = json[pos];
const escapeMap: Record<string, string> = {
'"': '"', '\\': '\\', '/': '/',
'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t',
};

if (escaped in escapeMap) {
result += escapeMap[escaped];
} else if (escaped === 'u') {
// Unicode 转义:\uXXXX
const hex = json.substring(pos + 1, pos + 5);
result += String.fromCharCode(parseInt(hex, 16));
pos += 4;
}
} else {
result += char;
}

pos++;
}

throw new SyntaxError('Unterminated string');
}

function parseNumber(): number {
const start = pos;

if (json[pos] === '-') pos++;
while (pos < json.length && json[pos] >= '0' && json[pos] <= '9') pos++;
if (json[pos] === '.') {
pos++;
while (pos < json.length && json[pos] >= '0' && json[pos] <= '9') pos++;
}
// 科学计数法 e/E
if (json[pos] === 'e' || json[pos] === 'E') {
pos++;
if (json[pos] === '+' || json[pos] === '-') pos++;
while (pos < json.length && json[pos] >= '0' && json[pos] <= '9') pos++;
}

return Number(json.substring(start, pos));
}

function parseObject(): Record<string, any> {
pos++; // 跳过 {
skipWhitespace();

const obj: Record<string, any> = {};

if (json[pos] === '}') {
pos++;
return obj;
}

while (true) {
skipWhitespace();
const key = parseString(); // key 必须是字符串

skipWhitespace();
expect(':');
skipWhitespace();

const value = parseValue();
obj[key] = value;

skipWhitespace();
if (json[pos] === ',') {
pos++;
} else if (json[pos] === '}') {
pos++;
break;
} else {
throw new SyntaxError(`Expected ',' or '}' at position ${pos}`);
}
}

return obj;
}

function parseArray(): any[] {
pos++; // 跳过 [
skipWhitespace();

const arr: any[] = [];

if (json[pos] === ']') {
pos++;
return arr;
}

while (true) {
skipWhitespace();
arr.push(parseValue());
skipWhitespace();

if (json[pos] === ',') {
pos++;
} else if (json[pos] === ']') {
pos++;
break;
} else {
throw new SyntaxError(`Expected ',' or ']' at position ${pos}`);
}
}

return arr;
}

function parseBoolean(): boolean {
if (json.startsWith('true', pos)) { pos += 4; return true; }
if (json.startsWith('false', pos)) { pos += 5; return false; }
throw new SyntaxError(`Invalid boolean at position ${pos}`);
}

function parseNull(): null {
if (json.startsWith('null', pos)) { pos += 4; return null; }
throw new SyntaxError(`Invalid null at position ${pos}`);
}

function skipWhitespace(): void {
while (pos < json.length && ' \t\n\r'.includes(json[pos])) pos++;
}

function expect(char: string): void {
if (json[pos] !== char) {
throw new SyntaxError(`Expected '${char}' at position ${pos}`);
}
pos++;
}

const result = parseValue();
skipWhitespace();

if (pos < json.length) {
throw new SyntaxError(`Unexpected data after position ${pos}`);
}

return result;
}

方法二:利用 Function 构造器(取巧,了解即可)

jsonParseEval.ts
function myJsonParse(json: string): any {
// 安全性较差,但面试中可以提及
return new Function('return ' + json)();
}
安全风险

这种方式等同于 eval(),会执行任意代码,存在 XSS 风险。永远不要在生产环境使用。面试中提及只是为了展示对 JavaScript 的理解。


常见面试追问

Q1: JSON.stringify 如何实现?

答案

function myJsonStringify(value: any): string {
if (value === null) return 'null';
if (value === undefined) return undefined as any; // JSON 不支持 undefined

const type = typeof value;

if (type === 'boolean' || type === 'number') {
return String(value);
}

if (type === 'string') {
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}

if (Array.isArray(value)) {
const items = value.map(item => {
const str = myJsonStringify(item);
return str === undefined ? 'null' : str; // undefined → null
});
return `[${items.join(',')}]`;
}

if (type === 'object') {
const entries: string[] = [];
for (const key of Object.keys(value)) {
const val = myJsonStringify(value[key]);
if (val !== undefined) { // 跳过 undefined
entries.push(`"${key}":${val}`);
}
}
return `{${entries.join(',')}}`;
}

return undefined as any; // function, Symbol 等返回 undefined
}

Q2: JSON 规范有哪些容易忽略的点?

答案

易错点说明
key 必须用双引号{name: 1} 是无效 JSON
不支持单引号{'a': 1} 无效
不支持尾逗号[1, 2,] 无效
不支持注释// comment/* */ 无效
不支持 undefined序列化时被忽略
NaN / Infinity序列化为 null
不支持 BigInt抛出 TypeError

Q3: 这与编译原理有什么关系?

答案:JSON 解析器就是一个递归下降解析器(Recursive Descent Parser)的简化版本:

JSON 文法:
value → object | array | string | number | "true" | "false" | "null"
object → "{" (pair ("," pair)*)? "}"
pair → string ":" value
array → "[" (value ("," value)*)? "]"

这和 Babel 解析 JavaScript、编程语言编译器的工作原理一样,只是 JSON 的文法非常简单。

掌握了 JSON 解析器的思路,就能理解:

  • Babel: 把 JS 源代码解析成 AST
  • 模板引擎: 解析 {{ expression }}
  • CSS 解析器: 解析选择器和属性

相关链接