模板引擎:二、實現一個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
轉義字元也未作處理。如果有興趣,可以繼續深入研究下去。謝謝!
可以參考下面的原始碼進行對比學習