2.Js实现Json解析器
前言
本文主要对Json解析器的实现进行探讨。
如果想深入了解其原理,可以参考上一篇文章:模板引擎:一、理解Json解析器工作原理
项目github地址:https://github.com/yang657850144/parseJson
案例说明
例如:拿一段最简单的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
转义字符也未作处理。如果有兴趣,可以继续深入研究下去。谢谢!
可以参考下面的源码进行对比学习
本文github项目地址:https://github.com/yang657850144/parseJson