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 許可證)