如何手写实现 JSON Parser
JSON.parse 是我们在前端开发中经常会用到API,如果我们要自己实现一个JSON.parse,我们应该怎么实现呢?今天我们就试着手写一个JSON Parser,了解下其内部实现原理。
JSON语法
JSON 是一种语法,用来序列化对象、数组、数值、字符串、布尔值和 null 。语法规则如下:
- 数据使用名/值对表示。
- 使用大括号({})保存对象,每个名称后面跟着一个 ':'(冒号),名/值对使用 ,(逗号)分割。

- 使用方括号([])保存数组,数组值使用 ,(逗号)分割。

- JSON值可以是:数字(整数或浮点数)/字符串(在双引号中)/逻辑值(true 或 false)/数组(在方括号中)/对象(在花括号中)/null

实现Parser
Parser 一般会经过下面几个过程,分为词法分析 、语法分析、转换、代码生成过程。
词法分析

通过对 JSON 语法的了解,我们可以看到 JSON 中会有一下类型及其特征如下表:
| 类型 | 基本特征 |
|---|---|
| Object | "{" ":" "," "}" |
| Array | "[" "," "]" |
| String | '"' |
| Number | "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" |
| Boolean | "true" "false" |
| Null | "null" |
所以根据这些特征,对 JSON 字符串进行遍历操作并与上述特征进行对比可以得到相应的 token。词法分析实现代码如下:
// 词法分析
const TokenTypes = {
OPEN_OBJECT: '{',
CLOSE_OBJECT: '}',
OPEN_ARRAY: '[',
CLOSE_ARRAY: ']',
STRING: 'string',
NUMBER: 'number',
TRUE: 'true',
FALSE: 'false',
NULL: 'null',
COLON: ':',
COMMA: ',',
}
class Lexer {
constructor(json) {
this._json = json
this._index = 0
this._tokenList = []
}
createToken(type, value) {
return { type, value: value || type }
}
getToken() {
while (this._index < this._json.length) {
const token = this.bigbang()
this._tokenList.push(token)
}
return this._tokenList
}
bigbang() {
const key = this._json[this._index]
switch (key) {
case ' ':
this._index++
return this.bigbang()
case '{':
this._index++
return this.createToken(TokenTypes.OPEN_OBJECT)
case '}':
this._index++
return this.createToken(TokenTypes.CLOSE_OBJECT)
case '[':
this._index++
return this.createToken(TokenTypes.OPEN_ARRAY)
case ']':
this._index++
return this.createToken(TokenTypes.CLOSE_ARRAY)
case ':':
this._index++
return this.createToken(TokenTypes.COLON)
case ',':
this._index++
return this.createToken(TokenTypes.COMMA)
case '"':
return this.parseString()
}
// number
if (this.isNumber(key)) {
return this.parseNumber()
}
// true false null
const result = this.parseKeyword(key)
if (result.isKeyword) {
return this.createToken(TokenTypes[result.keyword])
}
}
isNumber(key) {
return key >= '0' && key <= '9'
}
parseString() {
this._index++
let key = ''
while (this._index < this._json.length && this._json[this._index] !== '"') {
key += this._json[this._index]
this._index++
}
this._index++
return this.createToken(TokenTypes.STRING, key)
}
parseNumber() {
let key = ''
while (this._index < this._json.length && '0' <= this._json[this._index] && this._json[this._index] <= '9') {
key += this._json[this._index]
this._index++
}
return this.createToken(TokenTypes.NUMBER, Number(key))
}
parseKeyword(key) {
let isKeyword = false
let keyword = ''
switch (key) {
case 't':
isKeyword = this._json.slice(this._index, this._index + 4) === 'true'
keyword = 'TRUE'
break
case 'f':
isKeyword = this._json.slice(this._index, this._index + 5) === 'false'
keyword = 'FALSE'
break
case 'n':
isKeyword = this._json.slice(this._index, this._index + 4) === 'null'
keyword = 'NULL'
break
}
this._index += keyword.length
return {
isKeyword,
keyword,
}
}
}
语法分析

语法分析是遍历每个 Token,寻找语法信息,并且构建一个叫做 AST(抽象语法树)的对象。在正式进行语法分析前,我们针对 JSON 的语法特征创建不同的类来记录 AST 上每个节点的信息。
class NumericLiteral {
constructor(type, value) {
this.type = type
this.value = value
}
}
class StringLiteral {
constructor(type, value) {
this.type = type
this.value = value
}
}
class BooleanLiteral {
constructor(type, value) {
this.type = type
this.value = value
}
}
class NullLiteral {
constructor(type, value) {
this.type = type
this.value = value
}
}
class ArrayExpression {
constructor(type, elements) {
this.type = type
this.elements = elements || []
}
}
class ObjectExpression {
constructor(type, properties) {
this.type = type
this.properties = [] || properties
}
}
class ObjectProperty {
constructor(type, key, value) {
this.type = type
this.key = key
this.value = value
}
}
接下来正式进行语法分析,对 Token 进行遍历并对其类型进行检查,创建节点信息,构建一个 AST(抽象语法树)的对象。代码如下:
// 语法分析
class Parser {
constructor(tokens) {
this._tokens = tokens
this._index = 0
this.node = null
}
jump() {
this._index++
}
getValue() {
const value = this._tokens[this._index].value
this._index++
return value
}
parse() {
const type = this._tokens[this._index].type
const value = this.getValue()
switch (type) {
case TokenTypes.OPEN_ARRAY:
const array = this.parseArray()
this.jump()
return array
case TokenTypes.OPEN_OBJECT:
const object = this.parseObject()
this.jump()
return object
case TokenTypes.STRING:
return new StringLiteral('StringLiteral', value)
case TokenTypes.NUMBER:
return new NumericLiteral('NumericLiteral', Number(value))
case TokenTypes.TRUE:
return new BooleanLiteral('BooleanLiteral', true)
case TokenTypes.FALSE:
return new BooleanLiteral('BooleanLiteral', false)
case TokenTypes.NULL:
return new NullLiteral('NullLiteral', null)
}
}
parseArray() {
const _array = new ArrayExpression('ArrayExpression')
while(true) {
const value = this.parse()
_array.elements.push(value)
if (this._tokens[this._index].type !== TokenTypes.COMMA) break
this.jump() // 跳过 ,
}
return _array
}
parseObject() {
const _object = new ObjectExpression('ObjectExpression')
_object.properties = []
while(true) {
const key = this.parse()
this.jump() // 跳过 :
const value = this.parse()
const property = new ObjectProperty('ObjectProperty', key, value)
_object.properties.push(property)
if (this._tokens[this._index].type !== TokenTypes.COMMA) break
this.jump() // 跳过 ,
}
return _object
}
}
转换
经过语法分析后得到了 AST,转换阶段可以对树节点进行增删改等操作,转换为新的 AST 树。
代码生成
生成代码阶段,是对转换后的 AST 进行遍历,根据每个节点的语法信息转换成最终的代码。
// 代码生成
class Generate {
constructor(tree) {
this.tree = tree
}
getResult() {
let result = this.getData(this.tree)
return result
}
getData(data) {
if (data.type === 'ArrayExpression') {
let result = []
data.elements.map(item => {
let element = this.getData(item)
result.push(element)
})
return result
}
if (data.type === 'ObjectExpression') {
let result = {}
data.properties.map(item => {
let key = this.getData(item.key)
let value = this.getData(item.value)
result[key] = value
})
return result
}
if (data.type === 'ObjectProperty') {
return this.getData(data)
}
if (data.type === 'NumericLiteral') {
return data.value
}
if (data.type === 'StringLiteral') {
return data.value
}
if (data.type === 'BooleanLiteral') {
return data.value
}
if (data.type === 'NullLiteral') {
return data.value
}
}
}
使用
function JsonParse(b) {
const lexer = new Lexer(b)
const tokens = lexer.getToken() // 获取Token
const parser = new Parser(tokens)
const tree = parser.parse() // 生成语法树
const generate = new Generate(tree)
const result = generate.getResult() // 生成代码
return result
}
总结
至此我们就实现了一个简单的 JSON Parse 解析器,通过对 JSON Parse 实现的探究,我们可以总结出此类解析器的实现步骤,首先对目标值的语法进行了解,提取其特征,然后通过词法分析,与目标特征进行比对得到 token,然后对 token 进行语法分析生成 AST(抽象语法树),再对 AST 进行增删改等操作,生成新的 AST,最终对 AST 进行遍历就会生成我们需要的目标值。
参考
- https://www.json.org/json-en.html
- https://lihautan.com/json-parser-with-javascript/
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON
最后
欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star
- 大数据分布式任务调度系统——Taier
- 轻量级的 Web IDE UI 框架——Molecule
- 针对大数据领域的 SQL Parser 项目——dt-sql-parser
- 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
- 一个速度更快、配置更灵活、使用更简单的模块打包器——ko
- 一个针对 antd 的组件测试工具库——ant-design-testing
如何手写实现 JSON Parser的更多相关文章
- 手写一个json格式化 api
最近写的一个东西需要对json字符串进行格式化然后显示在网页上面. 我就想去网上找找有没有这样的api可以直接调用.百度 json api ,搜索结果都是那种只能在网页上进行校验的工具,没有api. ...
- 手写Json解析器学习心得
一. 介绍 一周前,老同学阿立给我转了一篇知乎回答,答主说检验一门语言是否掌握的标准是实现一个Json解析器,网易游戏过去的Python入门培训作业之一就是五天时间实现一个Json解析器. 知乎回答- ...
- 为sproto手写了一个python parser
这是sproto系列文章的第三篇,可以参考前面的<为sproto添加python绑定>.<为python-sproto添加map支持>. sproto是云风设计的序列化协议,用 ...
- 『练手』手写一个独立Json算法 JsonHelper
背景: > 一直使用 Newtonsoft.Json.dll 也算挺稳定的. > 但这个框架也挺闹心的: > 1.影响编译失败:https://www.cnblogs.com/zih ...
- 手写Json转换
在做项目的时候总是要手动将集合转换成json每次都很麻烦,于是就尝试着写了一个公用的方法,用于转换List to json: using System; using System.Collection ...
- Atitit s2018.2 s2 doc list on home ntpc.docx \Atiitt uke制度体系 法律 法规 规章 条例 国王诏书.docx \Atiitt 手写文字识别 讯飞科大 语音云.docx \Atitit 代码托管与虚拟主机.docx \Atitit 企业文化 每日心灵 鸡汤 值班 发布.docx \Atitit 几大研发体系对比 Stage-Gat
Atitit s2018.2 s2 doc list on home ntpc.docx \Atiitt uke制度体系 法律 法规 规章 条例 国王诏书.docx \Atiitt 手写文字识别 ...
- 用 F# 手写 TypeScript 转 C# 类型绑定生成器
前言 我们经常会遇到这样的事情:有时候我们找到了一个库,但是这个库是用 TypeScript 写的,但是我们想在 C# 调用,于是我们需要设法将原来的 TypeScript 类型声明翻译成 C# 的代 ...
- 手写webpack核心原理,再也不怕面试官问我webpack原理
手写webpack核心原理 目录 手写webpack核心原理 一.核心打包原理 1.1 打包的主要流程如下 1.2 具体细节 二.基本准备工作 三.获取模块内容 四.分析模块 五.收集依赖 六.ES6 ...
- 手写一个webpack,看看AST怎么用
本文开始我会围绕webpack和babel写一系列的工程化文章,这两个工具我虽然天天用,但是对他们的原理理解的其实不是很深入,写这些文章的过程其实也是我深入学习的过程.由于webpack和babel的 ...
- 一起写一个JSON解析器
[本篇博文会介绍JSON解析的原理与实现,并一步一步写出来一个简单但实用的JSON解析器,项目地址:SimpleJSON.希望通过这篇博文,能让我们以后与JSON打交道时更加得心应手.由于个人水平有限 ...
随机推荐
- 预设型 DP
预设型 DP <美好的一天>--青春学概论 한 잔 술에 취해 잠긴 목엔 沉醉于一杯酒 갈라지는 목소린 다시 带着沙哑的嗓音 두 잔 자기 전엔 기분 좋음 入睡前饮下第二杯让心情愉悦 ...
- java_GUI_监听事件关闭窗口_AWT
package Panel;import java.awt.*;import java.awt.event.WindowAdapter;import java.awt.event.WindowEven ...
- uni-app 解析支付宝form表单,h5 app唤起支付宝
1.通过接口拿到form表单 code为后端返回的form表单数据: document则是使用 document.querySelector('body').innerHTML 生成的html页面: ...
- 全网最适合入门的面向对象编程教程:44 Python内置函数与魔法方法-重写内置类型的魔法方法
全网最适合入门的面向对象编程教程:44 Python 内置函数与魔法方法-重写内置类型的魔法方法 摘要: 在 Python 中,内置类型的行为是通过一组特殊的"魔法方法"来实现的, ...
- 无法加载nodejs\vue.ps1
发现问题 刚换了电脑之后,安装了node.js.vue/cli,在vscode中使用vue ui命令新建vue项目时,发现报错如下: 分析问题 多番查询后发现,一般此类问题大多出现在第一次运行脚本的电 ...
- OpenAI GPT-4发布总结
OpenAI官方发布了GPT-4模型,GPT-4是一个大型多模态模型,支持输入文本+图片,输出还是文字形式,GPT-4比任何 GPT-3.5 模型都更强大,能够执行更复杂的任务,并针对聊天进行了优化. ...
- 技术解析 | ZEGO 移动端超分辨率技术
即构超分追求:速度更快.效果更好.码率更低.机型更广. 超分辨率(Super Resolution, SR)是从给定的低分辨率(Low Resolution, LR)图像中恢复高分辨率(High ...
- Angular 学习笔记 (Typescript 高级篇)
由于 typescript 越来越复杂. 所以特意开多一个篇幅来记入一些比较难的, 和一些到了一定程度需要知道的基础. 主要参考 https://basarat.gitbook.io/typescri ...
- python3 numpy的一些小知识点
简介 一个用python实现的科学计算,包括: 1.一个强大的N维数组对象Array: 2.比较成熟的(广播)函数库: 3.用于整合C/C++和Fortran代码的工具包: 4.实用的线性代数.傅里叶 ...
- MyBatis动态增改,多字段模糊查询
示例: insert insert into bargain_products <trim prefix="(" suffix=")" suffixOve ...