JSON.parse
是浏览器内置的 API,但若是面试官让你实现一个怎么办?好在有人已经帮忙作了这件事,本周咱们一块儿精读这篇 JSON Parser with Javascript 文章吧,再温习一遍大学时编译原理相关知识。javascript
要解析 JSON 首先要理解语法概念,以前的 精读《手写 SQL 编译器 - 语法分析》 系列也有介绍过,不过本文介绍的更形象,看下面这个语法图:前端
这是关于 Object 类型的语法描述图,从左向右看,根据箭头指向只要能走出这个迷宫就属于正确语法。java
好比第一行 {
→ whitespace
→ }
表示 { }
属于合法的 JSON 语法。git
再好比观察向下的一条最长路线:{
→ whitespace
→ string
→ whitespace
→ :
→ value
→ }
表示 { string : value }
属于合法的 JSON 语法。github
你可能会问,双引号去哪儿了?这就是语法树最核心的概念了,这张图是关于 Object 类型的 产生式,同理还有 string、value 的产生式,产生式中能够嵌套其余产生式,甚至造成环路,以此拥有描述纷繁多变语法的能力。面试
最后咱们再看一个环路,即 {
→ whitespace
→ string
... ,
→ whitespace
→ string
... ,
... }
,咱们发现,只要不走回头路,这条路是能够一直 “绕圈” 下去的,所以 Object 类型拥有了任意数量子字段的能力,只是每造成一个子字段,必须通过 ,
号分割。json
首先实现一个基本结构:浏览器
function fakeParseJSON(str) {
let i = 0;
// TODO
}
复制代码
i
表示访问字符的下标,当 i
走到字符串结尾表示遍历结束。缓存
而后是下一步,用几个函数描述解析语法的过程:性能优化
function fakeParseJSON(str) {
let i = 0;
function parseObject() {
if (str[i] === '{') {
i++;
skipWhitespace();
// if it is not '}',
// we take the path of string -> whitespace -> ':' -> value -> ...
while (str[i] !== '}') {
const key = parseString();
skipWhitespace();
eatColon();
const value = parseValue();
}
}
}
}
复制代码
其中 skipWhitespace
表示匹配并跳过空格,所谓匹配意味着匹配成功,此时 i
下标能够继续后移,不然匹配失败。下一步则判断若是 i
不是结束标志 }
,则按照 parseString
匹配字符串 → skipWhitespace
跳过空格 → eatColon
吃掉逗号 → parseValue
匹配值,这个链路循环。其中吃掉逗号表示 “匹配逗号但不会产生任何结果,因此就像吃掉了同样”,吃这个动做还能够用在其余场景,好比吃掉尾分号。
对于看到这儿的小伙伴,笔者要友情提示一下,原文的思路是一种定制语法解析思路,不管是
eatColon
仍是parseValue
都仅具有解析 JSON 的通用性,但不具有解析任意语法的通用性。若是你想作一个具有解析任何通用语法的解析器,读入的内容应该是语法描述,处理方式必须更加通用,若是感兴趣能够阅读 精读《手写 SQL 编译器 - 语法分析》 系列文章了解更多。
因为 Object 第一个元素前面不容许加逗号,所以能够利用 initial
作一个初始化断定,在初始时机不会吃掉逗号:
function fakeParseJSON(str) {
let i = 0;
function parseObject() {
if (str[i] === '{') {
i++;
skipWhitespace();
let initial = true;
// if it is not '}',
// we take the path of string -> whitespace -> ':' -> value -> ...
while (str[i] !== '}') {
if (!initial) {
eatComma();
skipWhitespace();
}
const key = parseString();
skipWhitespace();
eatColon();
const value = parseValue();
initial = false;
}
// move to the next character of '}'
i++;
}
}
}
复制代码
那么当第一个子元素前面存在逗号时,因为没有 “吃掉逗号” 这个功能,因此读到逗号会报错,语法解析提早结束。
吃逗号和吃冒号的代码都很是简单,即判断当前字符串必须是 “要吃的那个元素”,而且在吃掉后将 i
下标自增 1:
function fakeParseJSON(str) {
// ...
function eatComma() {
if (str[i] !== ',') {
throw new Error('Expected ",".');
}
i++;
}
function eatColon() {
if (str[i] !== ':') {
throw new Error('Expected ":".');
}
i++;
}
}
复制代码
在有了基本断定功能后,fakeParseJSON
须要返回 Object,所以咱们只需在每一个循环中对 Object 赋值,最后一并 return 便可:
function fakeParseJSON(str) {
let i = 0;
function parseObject() {
if (str[i] === '{') {
i++;
skipWhitespace();
const result = {};
let initial = true;
// if it is not '}',
// we take the path of string -> whitespace -> ':' -> value -> ...
while (str[i] !== '}') {
if (!initial) {
eatComma();
skipWhitespace();
}
const key = parseString();
skipWhitespace();
eatColon();
const value = parseValue();
result[key] = value;
initial = false;
}
// move to the next character of '}'
i++;
return result;
}
}
}
复制代码
解析 Object 的代码就完成了。
接着试着解析 Array,下面是 Array 的语法图:
咱们只须要吃逗号和 parseValue
便可:
function fakeParseJSON(str) {
// ...
function parseArray() {
if (str[i] === '[') {
i++;
skipWhitespace();
const result = [];
let initial = true;
while (str[i] !== ']') {
if (!initial) {
eatComma();
}
const value = parseValue();
result.push(value);
initial = false;
}
// move to the next character of ']'
i++;
return result;
}
}
}
复制代码
接下来到了有趣的 value
语法图,能够看到 value
是许多种基础类型的 “或” 关系组成的:
咱们只须要继续拆解分析便可:
function fakeParseJSON(str) {
// ...
function parseValue() {
skipWhitespace();
const value =
parseString() ??
parseNumber() ??
parseObject() ??
parseArray() ??
parseKeyword('true', true) ??
parseKeyword('false', false) ??
parseKeyword('null', null);
skipWhitespace();
return value;
}
}
复制代码
其中 parseKeyword
函数用来解析一些保留关键字,好比将 "true"
解析成布尔类型 true
:
function fakeParseJSON(str) {
// ...
function parseKeyword(name, value) {
if (str.slice(i, i + name.length) === name) {
i += name.length;
return value;
}
}
}
复制代码
如上所示,只要在 name 与对应字符相等时,返回第二个传入参数便可。
一个完整的语法解析功能须要包含错误处理,错误的状况主要分两种:
原文提到的 JSON 错误提示优化很是棒,想一想你在开发中忽然看到下面的提示,是否是很蒙圈:
Unexpected token "a"
复制代码
既然咱们是本身写的 JSON 解析器,就能够进行更友好的异常提示,好比:
// show
{ "b"a
^
JSON_ERROR_001 Unexpected token "a".
Expecting a ":" over here, eg:
{ "b": "bar" }
^
You can learn more about valid JSON string in http://goo.gl/xxxxx
复制代码
更多 Demo 能够查看 原文。
这篇文章经过一个具体的例子解释如何作语法分析,对于词法解析入门很是直观,若是你想更深刻理解语法解析,或者写一个通用语法解析器,能够阅读语法解析系列入门文章,笔者经过实际例子带你一步一步作一个完备的词法解析工具!
语法解析入门系列文章,建议阅读顺序:
syntax-parser 这个零依赖的通用语法解析库就是根据上述文章一步一步完成的,看完了上面文章,就完全理解了这个库的源码。
若是你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)