在 json.org/json-zh.htm… 中介绍了JSON各类语法的状态机。本文根据这些状态机给出解析JSON的typescript实现方法。
JSON 是一种非常简单的语言。有以下几个特点:
- JSON 有四种基本类型:字符串、数字、布尔类型和
null。 - JSON 有两个复杂类型:对象和数组。包含基本类型,并且可以相互嵌套。
- JSON 可以增加空白字符。以提高可读性。
- JSON 解析不需要前瞻。即根据当前字符便可以判断出当前是什么类型。比如检测到
[符号,就可以知道是一个数组。
在下文中,我们定义使用 parseValue 解析一个值,包括了基本类型和复杂类型;使用parseObject和parseArray 分别解析对象和数组;使用 parseNumber和parseString解析数字和字符串;使用 skipWhitespace 跳过空白字符。因为解析过程中不需要前瞻,所以我们只定义了 pos 来控制解析的进程,使用peek=s[pos]代表当前解析的字符。基础代码如下所示,其他方法都会定义到 Parse 类里面。
class Parse {
private pos = 0;
private get peek() {
return this.s[this.pos];
}
constructor(private s: string) {}
}
注:代码在Deno环境中开发,测试方法使用了 Deno.test。可以替换成任意的typescript环境和测试框架。本文的代码只可以作为学习使用,不可以在生产环境中使用。
skipWhitespace: 跳过空白符
上图是空白符的状态机。空白符包括了 space、linefeed、carriage feed 和 horizontal tab。分别代表了 空格、换行、回车 和 水平制表。
class Parse {
skipWhitespace() {
while (Parse.isWhitespace(this.peek)) {
this.pos++;
}
}
static isWhitespace(char: string) {
switch (char.charCodeAt(0)) {
case 32: // 空格 space
case 10: // 换行 linefeed
case 13: // 回车 carriage feed
case 9: // 水平符号 horizontal tab
return true;
default:
return false;
}
}
}
Deno.test("isWhitespace", () => {
assertEquals(Parse.isWhitespace(" "), true);
assertEquals(
Parse.isWhitespace(`
`),
true,
);
assertEquals(Parse.isWhitespace(" "), true);
assertEquals(Parse.isWhitespace("12"), false);
});
parseValue: 解析一个值
上图是基本类型的状态机。JSON解析时不需要前瞻,这也是为什么它的解析非常容易的原因。当我们碰到 [ 字符时,说明是一个数组;碰到 {,说明是一个对象;碰到 -,说明是一个负数,碰到 ",说明是一个字符串。碰到 t、n和f,说明是 true、null和false。我们按照这个顺序一步步往下解析,便可以将一个值解析完。
class Parse {
parseValue() {
this.skipWhitespace();
switch (this.peek) {
case "n":
this.pos += 4;
return null;
case "t":
this.pos += 4;
return true;
case "f":
this.pos += 5;
return false;
case "{":
return this.parseObject();
case "[":
return this.parseArray();
case '"':
return this.parseString();
case "-":
this.pos++;
return -this.parseNumber();
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9":
return this.parseNumber();
default:
throw "解析错误";
}
}
}
Deno.test("测试基础类型", () => {
assertEquals(new Parse("true").parseValue(), true);
assertEquals(new Parse("false").parseValue(), false);
assertEquals(new Parse('"123"').parseValue(), "123");
assertEquals(new Parse("1").parseValue(), 1);
assertEquals(new Parse("1.2").parseValue(), 1.2);
});
parseArray: 解析一个数组
上图是数组类型状态机。对应的代码如下。
class Parse {
parseArray() {
this.pos++; // 跳过 '['
this.skipWhitespace();
const list: unknown[] = [];
if (this.peek == "]") {
this.pos++;
return list;
}
while (true) {
this.skipWhitespace();
list.push(this.parseValue());
this.skipWhitespace();
if (this.peek == "]") {
this.pos++;
break;
}
if (this.peek == ",") {
this.pos++;
}
}
return list;
}
}
Deno.test("测试数组", () => {
assertEquals(new Parse("[1,2]").parseArray(), [1, 2]);
assertEquals(new Parse("[1,[2,3]]").parseArray(), [1, [2, 3]]);
assertEquals(new Parse("[1,[2,3,[1]]]").parseArray(), [1, [2, 3, [1]]]);
assertEquals(new Parse("[1,[2,3,[1],4,[5]]]").parseArray(), [1, [
2,
3,
[1],
4,
[5],
]]);
});
parseObject: 解析一个对象
上图是对象类型状态机。对应的代码如下。
class Parse {
parseObject() {
this.pos++; // skip '{'
this.skipWhitespace();
const values: any = {};
if (this.peek == "}") {
this.pos++;
return values;
}
while (true) {
this.skipWhitespace();
const key = this.parseString();
this.skipWhitespace();
if (this.peek != ":") {
throw "FormatException";
}
this.pos++;
this.skipWhitespace();
const value = this.parseValue();
this.skipWhitespace();
values[key] = value;
// @ts-ignore
if (this.peek === "}") {
this.pos++;
break;
}
// @ts-ignore
if (this.peek === ",") {
this.pos++;
}
}
return values;
}
}
parseString: 解析一个字符串
上图是字符串对应的状态机。为了简单表达,下面的代码没有解析特殊字符。如果读者感兴趣,可以自己实现。
class Parse {
parseString(): string {
this.pos++; //skip '"'
if (this.peek == '"') {
this.pos++;
return "";
}
var str = "";
while (true) {
if (this.peek == '"') {
break;
}
str += this.peek;
this.pos++;
}
this.pos++; //skip '"'
return str;
}
}
parseNumber: 解析一个数字
上图是数字对应的状态机。为了简单表达,下面的代码只解析了简单的整数和小数。如果读者感兴趣,可以自己实现。
class Parse {
parseNumber() {
let num = "";
while (Parse.isNumber(this.peek)) {
num += this.peek;
this.pos++;
}
if (this.peek === ".") {
this.pos++;
const decimal = this.parseNumber();
num += "." + decimal;
}
return parseFloat(num);
}
static isNumber(char: string) {
return char >= "0" && char <= "9";
}
}
parse: 解析一个JSON字符串
class Parse {
parse() {
this.skipWhitespace();
let json: any = this.s;
switch (this.peek) {
case "{":
json = this.parseObject();
break;
case "[":
json = this.parseArray();
break;
default:
json = this.parseValue();
}
this.skipWhitespace();
return json;
}
}
Deno.test("测试完整的一个JSON", () => {
const obj = {
"level": 2,
"tid": 73753,
"classId": 82,
"gameType": 6,
"uids": [
103136,
100113,
100778,
],
"info": [
{
"uid": 103136,
"winChips": 455700,
"userChips": 100372300,
"fee": -135000,
"blind": 300000,
},
{
"uid": 100113,
"winChips": -300000,
"userChips": 93139748,
"fee": 0,
"blind": 300.45,
},
{
"uid": 100778,
"winChips": -300000,
"userChips": 99100000,
"fee": 0,
"blind": 300000,
},
],
};
assertEquals(new Parse(JSON.stringify(obj)).parseObject(), obj);
});