JavaScript 實現 JSON 解析器

2021-02-20 WecTeam

原文地址:https://lihautan.com/json-parser-with-javascript/

原文作者:Tan Li Hau

譯者:龔亮

聲明:本翻譯僅做學習交流使用,轉載請註明來源。

本周 Cassidoo 每周時事通訊[1]的面試問題是:編寫一個函數,該函數接受一個有效的JSON字符串並將其轉換為一個對象。程式語言不限,數據結構不限。輸入示例:

fakeParseJSON('{ "data": { "fish": "cake", "array": [1,2,3], "children": [ { "something": "else" }, { "candy": "cane" }, { "sponge": "bob" } ] } } ')

有一次,我忍不住想寫:

const fakeParseJSON = JSON.parse;

但是,我想,我已經寫了不少關於 AST 的文章:

•使用Babel創建自定義JavaScript語法[2]•編寫自定義babel轉換的逐步指南[3]•用JavaScript操作AST[4]

其中包括編譯器管道的概述,以及如何操作 AST,但是我還沒有詳細介紹如何實現解析器。

這是因為在一篇文章中實現JavaScript編譯器對我來說是一項艱巨的任務。

好吧,不用擔心。JSON 也是一種語言。它具有自己的語法,您可以從規範[5]中參考。編寫 JSON 解析器所需的知識和技術可以轉移到編寫 JS 解析器中。

因此,讓我們開始編寫 JSON 解析器!

理解語法

如果您查看了規範頁面,會發現有2個圖。

•左側的語法圖(或者鐵路圖):


圖片來源:https://www.json.org/img/object.png

•右側的 McKeeman形式[6] ,是 Backus-Naur形式(BNF)[7] 的變體。

json  element
value object array string number "true" "false" "null"
object '{' ws '}' '{' members '}'

這兩個圖是等效的。

一個是可視化的,另一個是基於文本的。基於文本的語法( Backus-Naur 形式)通常被提供給另一個解析器,該解析器解析該語法並為其生成一個解析器。🤯

在本文中,我們將重點關注鐵路圖,因為它是可視化的,而且似乎對我更友好。

讓我們看看第一張鐵路圖:


圖片來源:https://www.json.org/img/object.png

這是 JSON 中「對象」的語法。

我們從左邊開始,沿著箭頭走,然後在右邊結束。

圓圈(例如:左花括號({),英文逗號(,),英文冒號(:),右花括號(}))是字符,方框(例如:空格(whitespace)、字符串(string)和值(value))是另一種語法的佔位符。如果要解析「空格」,我們需要查看空格的語法。

因此,對於一個對象,從左邊開始第一個字符必須是一個左花括號。然後我們有兩個選擇:

•空格 -> 右花括號 -> 結束, 或者•空格 -> 字符串 -> 空格 -> 英文冒號 -> 值 -> 右花括號 -> 結束

當然,當您到達「值」時,您可以選擇:

•-> 右花括號 -> 結束,或者•-> 英文逗號 -> 空格 -> ... -> 值

您可以繼續保持循環,直到您決定執行以下操作:

•-> 右花括號 -> 結束。

我想我們現在已經熟悉鐵路圖,讓我們繼續下一節。

實現解析器

讓我們從以下結構開始:

function fakeParseJSON(str) {  let i = 0;  // TODO}

我們初始化i作為當前字符的索引,當i到達str結束時,我們將立即結束。

讓我們實現「對象」的語法:

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(); } } }}

在parseObject中,我們將調用其他語法的解析,例如「字符串」和」空格」,當我們實現它們時,一切都會起作用🤞。

我忘了加上一個英文逗號,,,只出現在我們開始第二次循環空格 -> 字符串 -> 空格 -> : -> ...之前。

基於此,我們添加了以下行,注意第8,12~15,20行(譯者加):

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++; } }}

一些命名約定:

•當我們基於語法解析代碼並使用返回值時,我們調用parseSomething•當我們期望字符在那裡,但我們沒有使用字符時,我們調用eatSomething•字符不在那裡,但我們的程序是ok的,我們調用skipSomething

讓我們來實現eatComma和eatColon:

function fakeParseJSON(str) {  // ...  function eatComma() {    if (str[i] !== ',') {      throw new Error('Expected ",".');    }    i++;  }
function eatColon() { if (str[i] !== ':') { throw new Error('Expected ":".'); } i++; }}

我們已經完成了parseObject語法的實現,但是這個解析函數的返回值是什麼呢?

我們需要返回一個 JavaScript 對象,注意第8,22,28行(譯者加)。

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; } }}

既然您已經看到我實現了「對象」語法,現在輪到您嘗試實現一下「數組」語法了:

圖片來源:https://www.json.org/img/array.png

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; } }}

現在進入一個更有趣的語法「值」。


圖片來源:https://www.json.org/img/value.png

值是以「空格」開始,然後是以下任意一種:「字符串」,「數字」,「對象」,「數組」,「真」,「假」或「空」,然後以「空格」結尾:

function fakeParseJSON(str) {  // ...  function parseValue() {    skipWhitespace();    const value =      parseString() ??      parseNumber() ??      parseObject() ??      parseArray() ??      parseKeyword('true', true) ??      parseKeyword('false', false) ??      parseKeyword('null', null);    skipWhitespace();    return value;  }}

??是 空值合併操作符[8],它就像||,我們通常使用foo || default設置默認值。我們期望當foo是假值時||返回default。然而只有當foo是null或者undefined時空值合併操作符返回default。

parseKeyword 將檢查當前的str.slice(i)是否與關鍵字字符串匹配,如果匹配,將返回關鍵字值:

function fakeParseJSON(str) {  // ...  function parseKeyword(name, value) {    if (str.slice(i, i + name.length) === name) {      i += name.length;      return value;    }  }}

parseValue就是這樣!

我們還有3種語法,但是我將節省本文的篇幅,並在下面的 CodeSandbox 中實現它們:

<iframe src="https://codesandbox.io/embed/json-parser-k4c3w?expanddevtools=1&fontsize=14&hidenavigation=1&theme=dark&view=editor" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="JSON解析器" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>

在我們完成所有語法實現之後,現在讓我們返回json的值,它是由parseValue返回的:

function fakeParseJSON(str) {  let i = 0;  return parseValue();
// ...}

就是這樣!

好吧,別急,我的朋友,我們剛剛完成了理想的情況,那異常的情況呢?

處理意外的輸入

作為一名優秀的開發人員,我們還需要優雅地處理異常情況。對於解析器,這意味著使用適當的錯誤消息對開發人員進行提醒。

讓我們處理兩種最常見的錯誤情況:

•意外的標記•字符串意外結束

意外的標記字符串意外結束

在所有的while循環中,比如parseObject中while循環:

function fakeParseJSON(str) {  // ...  function parseObject() {    // ...    while(str[i] !== '}') {

我們需要確保訪問的字符不會超過字符串的長度。在這個例子中,這發生在字符串意外結束時,而我們仍然在等待一個結束字符「}」。

function fakeParseJSON(str) {  // ...  function parseObject() {    // ...    while (i < str.length && str[i] !== '}') {      // ...    }    checkUnexpectedEndOfInput();
// move to the next character of '}' i++;
return result; }}

加倍努力

您還記得您還是一名初級開發人員的時候,每當您遇到帶有加密消息的語法錯誤時,您完全不知道出了什麼問題嗎?現在您有了更多經驗,該停止這個良性循環並停止大喊大叫了。

並讓用戶呆呆地盯著屏幕。

有很多比大喊大叫來處理錯誤消息的更好的方法,您可以考慮將以下幾點添加到解析器中:

錯誤代碼和標準錯誤消息

這對於用戶向 Google 尋求幫助作為標準關鍵字很有用。

// instead ofUnexpected token "a"Unexpected end of input
// showJSON_ERROR_001 Unexpected token "a"JSON_ERROR_002 Unexpected end of input

更好地了解出了什麼問題

像 Babel 這樣的解析器,將向您顯示一個代碼框架,一個帶有下劃線、箭頭或突出顯示錯誤的代碼片段:

// instead ofUnexpected token "a" at position 5
// show{ "b"a ^JSON_ERROR_001 Unexpected token "a"

有關如何列印代碼段的示例:

function fakeParseJSON(str) {  // ...  function printCodeSnippet() {    const from = Math.max(0, i - 10);    const trimmed = from > 0;    const padding = (trimmed ? 3 : 0) + (i - from);    const snippet = [      (trimmed ? '...' : '') + str.slice(from, i + 1),      ' '.repeat(padding) + '^',      ' '.repeat(padding) + message,    ].join('\n');    console.log(snippet);  }}

錯誤恢復建議

如果可能,請解釋出了什麼問題,並提供有關如何解決它們的建議:

// instead ofUnexpected token "a" at position 5
// 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

如果可能,請根據解析器到目前為止收集的上下文提供建議:

fakeParseJSON('"Lorem ipsum');
// instead ofExpecting a `"` over here, eg:"Foo Bar" ^
// showExpecting a `"` over here, eg:"Lorem ipsum" ^

基於上下文的建議會讓人感覺更有共鳴和可操作。

記住所有的建議,檢查更新的 CodeSandbox。

•有意義的錯誤消息•帶有錯誤指向失敗點的代碼段•提供錯誤恢復建議

<iframe src="https://codesandbox.io/embed/json-parser-hjwxk?expanddevtools=1&fontsize=14&hidenavigation=1&theme=dark&view=editor" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="JSON解析器(帶有錯誤處理)" allow="geolocation; microphone; camera; midi; vr; accelerometer; gyroscope; payment; ambient-light-sensor; encrypted-media; usb" sandbox="allow-modals allow-forms allow-popups allow-scripts allow-same-origin"></iframe>

總結

要實現解析器,您需要從語法開始。

您可以使用鐵路圖或 Backus-Naur 形式語法。設計語法是最難的一步。

一旦掌握了語法,就可以開始基於語法來實現解析器。

錯誤處理很重要,更重要的是擁有有意義的錯誤消息,以便用戶知道如何解決它。

現在您知道了如何實現簡單的解析器,是時候著眼於更複雜的解析器了。

•Babel parser•Svelte parser

最後,請關注 @cassidoo[9] ,她的每周時事通訊棒極了!

感謝您花時間閱讀本文。這對我意義重大。

如果你喜歡你剛剛讀到的,請在 Tweet 轉發[10]並評論它,我會寫更多相關的文章;

如果你不同意或對這篇文章有意見,也請在 Tweet 轉發[11]並評論它,我可以採納你的建議並改進它。

References

[1] Cassidoo 每周時事通訊: https://cassidoo.co/newsletter/confirmed.html
[2] 使用Babel創建自定義JavaScript語法: https://lihautan.com/creating-custom-javascript-syntax-with-babel/
[3] 編寫自定義babel轉換的逐步指南: https://lihautan.com/step-by-step-guide-for-writing-a-babel-transformation/
[4] 用JavaScript操作AST: https://lihautan.com/manipulating-ast-with-javascript/
[5] 規範: https://www.json.org/json-en.html
[6] McKeeman形式: https://www.crockford.com/mckeeman.html
[7] Backus-Naur形式(BNF): https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form
[8] 空值合併操作符: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator
[9] @cassidoo: https://twitter.com/cassidoo
[10] 轉發: https://twitter.com/intent/tweet?text=I%20disgree%20with%20%40lihautan's%20article&url=https://lihautan.com/json-parser-with-javascript/#unexpected-token"
[11] 轉發: https://twitter.com/intent/tweet?text=I%20disgree%20with%20%40lihautan's%20article&url=https://lihautan.com/json-parser-with-javascript/#unexpected-token"

相關焦點

  • 面試題|手寫JSON解析器
    JavaScript語法一步步教你實現一個Babel轉換器使用JavaScript操作AST其中涵蓋了編譯器管道的概述以及如何操作AST,但是我沒有過多介紹如何實現解析器。因為實現JavaScript編譯器對我來說是一項艱巨的任務。那就沒必要擔心。 JSON也是一種語言,有自己的語法,可以參考規範。 根據編寫JSON解析器所需的知識和技術轉移到編寫JS解析器中。好了,那就開始編寫一個JSON解析器吧。語法查看規範文檔頁面,可以看到以下兩個圖。下面的語法圖(或者叫鐵路圖)
  • 擼一個JSON解析器
    JSON存在以下幾種數據類型(以Java做類比):jsonjavastringJava中的StringnumberJava中的Long或Doubletrue/falseJava中的BooleannullJava中的null[array]Java中的List或Object[]{「key」:」value」}Java中的Map<String, Object>解析JSONJSON解析器的基本原理
  • 擼一個 JSON 解析器
    通過上面的了解可以看出,JSON存在以下幾種數據類型(以Java做類比):jsonjavastringJava中的StringnumberJava中的Long或Doubletrue/falseJava中的BooleannullJava中的null[array]Java中的List或Object[]{「key」:」value」}Java中的Map解析JSONJSON解析器的基本原理
  • PHP中JSON的應用
    XML的解析,恐怕已經不是什麼難題了,特別是PHP5,大量的XML解析器的湧現,如最輕量級的SimpleXML。不過對於AJAX來說,XML的解析更傾向於前臺Javascript的支持度。我想所有解析過XML的人,都會因樹和節點而頭大。不可否認,XML是很不錯的數據存儲方式,但是其靈活恰恰造成了其解析的困難。當然,這裡所指的困難,是相對於本文的主角--JSON而言。JSON為何物?
  • 自己手擼一個 JSON 解析器
    },    {            "姓名": "裡斯",              "年齡":"19"       }]通過上面的了解可以看出,JSON存在以下幾種數據類型(以Java做類比):Java中的Map<String, Object>解析JSONJSON解析器的基本原理步驟第一步
  • Jackson,最牛掰的 Java JSON 解析器
    Java 之所以牛逼,很大的功勞在於它的生態非常完備,JDK 沒有 JSON 庫,第三方類庫有啊,還挺不錯,比如說本篇的豬腳——Jackson,GitHub 上標星 6.1k,Spring Boot 的默認 JSON 解析器。怎麼證明這一點呢?
  • xml及json解析
    3.著重看下如何解析,做安卓開發主要還是用,SAX  DOM  PULL首先簡單說下SAX和DOM解析的異同,而PULL解析是安卓特有,單獨說首先解析方式上,主要是以下兩種,幾乎所有商用的xml 解析器都同時實現了這兩個接口
  • 使用jQuery的ajax技術+JSON數據格式+C#+SQL Server實現數據顯示
    這裡使用jQuery的ajax技術+JSON格式的數據+SQL Server資料庫來實現數據以表格形式顯示的功能。">function loadData() {var d= $.ajax({url: "WebForm1.aspx/GetJsonData",dataType: "json",type: "GET",//當返回json格式時,contentType
  • 一個簡單的Ajax功能(用到Jquery與Json)
    list.add(user1);       list.add(user2);       Gson gson= new Gson();//這個需要導入第三方包(gson-2.2.2.jar)不然用不了       String str=gson.toJson(list);//把list對象轉成json
  • 重新認識javascript的settimeout和異步
    然後看了一下文章下面的評論,發現5樓和6樓的回答很有道理,主要意思就是說javascript引擎是單線程執行的,while循環那裡執行的時候,settimeout裡面的函數根本沒有執行的機會,這樣while那裡永遠為真,造成死循環。
  • 千萬不要用JSON作配置文件
    3、標準嚴格JSON標準相當嚴格,這是它的優點,用簡潔和快速的解析器,不必處理不同的格式,但這也意味著編寫起來更加困難。例如,對象或數組中的逗號是一個錯誤,並且已經多次困擾我。如果你的字符串中包含很多雙引號,那麼轉義所有的實例或者引號會非常麻煩。
  • Jackson用樹模型處理JSON是必備技能,不信你看
    樹模型樹模型是JSON數據內存樹的表示形式,這是最靈活的方法,它就類似於XML的DOM解析器。Jackson提供了樹模型API來「生成和解析」 JSON串,主要用到如下三個核心類:JsonNodeFactory:顧名思義,用來構造各種JsonNode節點的工廠。
  • 流轉json專題及常見問題 - CSDN
    >2)、將字符串傳入響應的JSON構造函數中 ①、通過構造函數將json字符串轉換成json對象 JSONObject jsonObject = new JSONObject(jsonStr); ②、通過構造函數將json字符串轉換成json數組: JSONArray array = new JSONArray(jsonStr); 3)、解析出JSON
  • 使用JAVA自已設計JSON解析器
    當然,有很多很好的JSON解析的JAR包,比如JSONOBJECT,GSON,甚至也有為我們測試人員而打造的JSONPATH,但我還是自已實現了一下
  • 如何正確運用PHP json_encode函數進行中文轉換
    如何正確運用PHP json_encode函數進行中文轉換 json_encode 和 json_decode這兩個函數的具體用法 網上有很多相關的文章 ,本文主要介紹
  • 使用JSONObject生成和解析json
    作者:joahyau連結:www.cnblogs.com/joahyau1. json數據類型json
  • 用javascript實現select的美化
    首頁 > 教程 > 關鍵詞 > 最新資訊 > 正文 用javascript實現select的美化
  • Windows phone如何實現json接口的調用
    目前QQ、新浪、搜狐微博都開放了API供開發人員調用,本文就是將從如何調用最簡單的json類型的接口開始逐步深入微博客戶端的開發。  因為各大客戶端的API調用都需要用戶傳入appkey和用戶名、密碼等信息,基於循序漸進的考慮,我們將從一個最簡單的第三方接口調用展開,以展示在WP7上是如何調用json類型接口的。
  • 20個常用的JavaScript簡寫技巧
    任何程式語言的簡寫技巧都能夠幫助你編寫更簡練的代碼,讓你用更少的代碼實現你的目標。讓我們一個個來看看 JavaScript 的簡寫技巧吧。 1. 聲明變量 2.
  • 數據類型和Json格式
    我馬上想到了json。21世紀初,Douglas Crockford尋找一種簡便的數據交換格式,能夠在伺服器之間交換數據。當時通用的數據交換語言是XML,但是Douglas Crockford覺得XML的生成和解析都太麻煩,所以他提出了一種簡化格式,也就是Json。