1. 程式人生 > >模板引擎:二、實現一個Json解析器

模板引擎:二、實現一個Json解析器

2.Js實現Json解析器

前言

本文主要對Json解析器的實現進行探討。
如果想深入瞭解其原理,可以參考上一篇文章:模板引擎:一、理解Json解析器工作原理

案例說明

例如:拿一段最簡單的Json字串舉例(“{ “a”: 1 }”),要將其解析為JSON物件。

我們先將其進行拆分取出字串中的特徵值(Token),我們可以得到下面七個Token:

    // 以逗號','進行分割
    ", {, "a", :, 1, }, "

然後,通過我們之前定義的資料結構進行匹配:

  • {},以一對大括號包裹的定義為一個物件,並且物件結構是以key-value形式進行儲存
  • “”, 以一對雙引號包裹的定義為字串
  • 1, 定義為數值型別

這樣,我們就識別出了我們想要的資料結構

{
    "a": 1
}

思路

通過上面的舉例,對Json解析器應該有了基本的理解。
但是,羅馬不是一天建成的。接下來我們將逐步完善Json解析器

識別關鍵字

下面再通過一段程式碼進行說明,先實現一個簡單的關鍵字解析器


// 定義關鍵字(Token)
const ENUM = {
    _TRUE: true,
    _FALSE: false,
    _NULL: null,
    _UNDEFINED: undefined
}

let at = 0
// 當前字元所在的下標 let ch = '' // 當前字元 let text = '' // 定義一個字串物件 /** * 定義一個字元掃描器 * params: char 傳入的為當前掃描的欄位 * return: 返回當前掃描(at)的一個字元(ch) **/ const getCharAt = (char) => { if(char && char !== ch) { console.error(`當前字元讀取錯誤: ${ch},錯誤位置: ${at}`) return } ch = text.charAt(at) // 讀取當前字元
at++ // 指標後移一位 return ch } /** * 關鍵字掃描器 * 功能描述: * 可識別字段(true,false,null,undefined) **/ const keyword = () => { // 通過首字母進行識別 switch(ch) { case 't': getCharAt('t') getCharAt('u') getCharAt('r') getCharAt('e') return ENUM._TRUE case 'f': getCharAt('f') getCharAt('a') getCharAt('l') getCharAt('s') getCharAt('e') return ENUM._FALSE case 'n': getCharAt('n') getCharAt('u') getCharAt('l') getCharAt('l') return ENUM._NULL case 'u': getCharAt('u') getCharAt('n') getCharAt('d') getCharAt('e') getCharAt('f') getCharAt('i') getCharAt('n') getCharAt('e') getCharAt('d') return ENUM._UNDEFINED } } /** * 源字串 * 測試用例: 'true','false','null','undefined' **/ text = 'null' // 呼叫關鍵字解析器 keyword() // 輸出: null

通過上面的關鍵字解析器,我們可以從源字串中識別出基本的幾個關鍵字
但是,這個解析器有一個缺陷,它只能精確識別諸如'false'、'null'等無空格的字串

如果字串中包含有多個空格(’  null’, ‘      false’),那麼我們的解析器就會失效了。

那麼,解決的思路有兩種

第一種,通過正則匹配,將字串中的空格進行過濾(str.replace(reg,''))
特點: 高效實用
另一種,實現過濾函式,如果當前字元是空格的話,跳過該字元,指標後移一位(at++)
特點:容易理解

我們通過第二種方式進行講解

// 接上面的程式碼
...

// 定義一個過濾函式
const filter = () => {
  while(ch & ch === ' ') {
    getCharAt()  // 如果當前字元為空格,指標後移一位 at++ 
  }
}


/** 
 * 源字串
 * 測試用例: '   true','   false','  null','  undefined'  
 **/
text = '   null'
// 呼叫過濾函式
filter()
// 呼叫關鍵字解析器
keyword() // 輸出: null

看到這裡,一個簡單的關鍵字解析器已經完成了。是不是有點小激動呢,哈哈,下面我們將慢慢考慮識別更多的資料結構了。

識別數值型別

數值型別的定義:

  • 正數
    • 整型
    • 浮點型
    • 指數型
  • 負數
    • 同上

考慮到篇幅有限,我們暫且只處理整型和浮點型的數值。

/**
 * 數值型別判斷
 * 
 **/
const number = () => {
    let str
    // 識別整型 
    while(ch && ch >= '0' && ch <= '9') {
        str += ch
        next()
    }
    // 識別浮點型
    if(ch === '.') {
        str += '.'
        next('.')
        while(next() && ch >= '0' && ch <= '9') {
            str += ch   
        }
    }
    return +str // 轉換為數值型

}



/** 
 * 源字串
 * 測試用例: '   1','   1.2','  12.34','1234'  
 **/
text = '  1.2'
// 呼叫過濾函式
filter()
// 呼叫數值解析器
number() // 輸出: 1.2

我們已經可以識別基本的數字型別了。

不過,下面有種情況,他們也屬於數值型別,但是解析器無法識別

+1
+1.2
-1
-1.2

不難看出,我們少了數值符號的判斷邏輯。因此,我們新增下面的符號條件判斷

/**
 * 數值符號
 * return 呼叫匹配的數值型別,並將符號傳入
 **/
const symbol = () => {
    if(ch === '+' || ch === '-') {
        let sym = ch // 識別以'+'、'-'起始的字元
        next(ch) // 指標後移
        if(ch && ch >= '0' && ch <= '9' ) {
            return number(sym) // 進入數值型別判斷
        }
    }
}

然後我們再重構我們的number函式

const number = (sym = '') => {
    // 邏輯不變
    ...
    return sym + (+str)

}

通過修改,我們又可以匹配諸如下面幾種有符號的數值型別了。

+1
+1.2
-1
-1.2

不過,number函式還是有一個Bug。

如果,輸入 1.2abc 或者1a2b 這類不合法的數值型別,我們必須對這種情況進行異常處理。

繼續重構我們的number函式

const number = () => {
    // 同上
    ...
    // return str + (+val)
    if(!isFinite(val)) {
        console.error(`無效的數值型別:${val}`)
    } else {
        return str + (+val)
    }

}

這樣,我們的Number函式就比較完善了。

識別字符串型別

字串定義,以一對”“包含的型別。

/**
 *  字串型別定義
 *  return 返回一個字串
 **/

 const string = () => {
    let str
    // " 起始
    if(ch === '"') {
        // 過濾空格
        filter()
        next('"')
        while(next()) {
            // “ 結尾
            if(ch === '"') {
                next('"')
                return str
            } else {            
                str += ch
            }
        }
    }
    console.error(`無效字串:${str},位置:${at}`)

/** 
 * 源字串
 * 測試用例: '"1"','"1a"','"   key"','"  1a."'  
 **/
text = '"   key"'
// 呼叫過濾函式
filter()
// 呼叫數值解析器
string() // 輸出: "key"
}

好了,到這裡基本資料型別講解完畢。我們將這三種資料型別整合到一個函式(getValue)中


const getValue = () => {
    filter()
    switch(ch) {
        case '"':
            return string()
        case '+':
        case '-':
            return symbol()
        case '[':
            return array()
        case '{':
            return object()
        default:
            return (ch && ch >='0' && ch <='9') ? number() : keyword()

    }

}

然後我們開始難度升級,對複合型別的處理(物件、陣列)

識別陣列

定義:以一對[]包裹,並以‘,’進行分割的資料型別。


const array = () => {
    let arr = []
    // 以 [ 起始
    if(ch && ch === '[') {
        next('[')
        filter() // 過濾空格
        // 識別為空陣列
        if(ch && ch === ']') {
            return arr
        }
        while(next()) {
            // 遞迴
            arr.push(getValue())
            if(ch === ']') {
                return arr
            }
            filter()
            // 以 , 將值進行分割
            if(ch === ',') {
                next(',')
            }
        }
    }
}

陣列匹配的難度在於遞迴的思想,去遍歷陣列中的各種資料型別。這也是處理複合型別的統一方法。

識別物件

與陣列的判斷方式型別,關鍵區別在於物件的資料格式是以”key-value形式儲存”。
而key則必須為一個基本資料型別,本文暫定為字串型別。

const object = () => {
    let obj = {}

    if(ch && ch === '{') {
        next('{')
        filter()
        //  空物件
        if(ch && ch === '}') {
            return obj
        }
        while(next()) {
            // 物件的key,型別為字串
            let key = string()
            filter()
            if(ch && ch === ':') {
                next(':')
                if(Object.hasOwnProperty.call(obj,key)) {
                    console.error(`物件關鍵字重複:${key}`)
                }
                // 遞迴獲取物件的value
                obj[key] = value()
                filter()
                if(ch && ch ==='}') {
                    next('}')
                    return obj
                }
                // 以 , 將key-value進行分割
                if(ch && ch === ',') {
                    next(',')
                }
            }
        }
    }

}

這樣,我們的基本Json物件就介紹完畢。

待改進部分

我們這個解析器對數值型別的判斷還是不夠準確。例如:2e10指數型別沒有正確識別。
以及,\t\n 轉義字元也未作處理。如果有興趣,可以繼續深入研究下去。謝謝!

可以參考下面的原始碼進行對比學習

這裡寫圖片描述