实现简易 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 解析器: 解析选择器和属性