如何手写实现 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打交道时更加得心应手.由于个人水平有限 ...
随机推荐
- k8s单机部署
一.环境 64位centos 二.安装 (1)关闭防火墙 systemctl stop firewalld systemctl disable firewalld (2)安装etcd与kubernet ...
- 搭建QT开发环境
下载 Qt官网,Qt下载网址 安装前要登录账号,其他的该咋就咋样,路径不能有中文. 组件自己选 我的是MinGW.Android.虚拟键盘.Qt脚本.Qt Creator 然后创个项目,能跑起来就是安 ...
- 【Python】Selenium自动化测试之动态识别验证码图片方法(附静态图片文字获取)
目录 一.前提 二.获取验证码 三.获取4位验证码 四.判断验证码是否正确 五.输入验证码登录 六.登录页面类 七.完整的获取验证码类代码 八.附录:静态图片文字提取 一.前提 返回目录 经常会遇到登 ...
- Seata 1.3.0 Oracle 回滚测试验证 报错 ORA-02289: 序列不存在
使用Seata 1.3.0版本,测试A服务调用B服务,且A方法中,手动写了一个异常,测试是否正常回滚(Mysql已经测试过) 发现报错:ORA-02289: 序列不存在 一看就是undo_log这张表 ...
- 6.9找回机制接口安全&验证码token接口
响应包response burp截取拦截,改相应包: 思路:此处应该若是修改密码,先抓到修改成功数据包(截取验证关键字),在替换为需要绕过的数据包,截取response数据包,修改验证成功关键字达到绕 ...
- CSS & JS Effect – Styling Select
参考 YouTube – Custom select menu - CSS only 原装 select 的缺点 这是一个原装 select design 它最大的问题是没有 spacing. bor ...
- RxJS 系列 – Observable & Creation Operators
前言 RxJS 最大篇幅就是一堆的 operators, 但是那些概念并不多, 只要常用就能熟能生巧了. Observable 和 Subject 反而需要我们了解清楚. 所以这篇我们先来了解这 2 ...
- ChatGPT转发工具-springboot
背景 国内服务器无法访问openAI接口,我想过有两种实现方式 代理工具类似 tinyproxy.nginx 开发一个转发客户端(java.python都可以实现),提供一个api接口 源码 gith ...
- .NET云原生应用实践(一):从搭建项目框架结构开始
开篇 很早之前就想做一套案例,介绍.NET下如何从零开始搭建一个云原生的应用程序.不过这个话题有点大,会要包含很多内容.我本打算从新建一个ASP.NET Core Web API应用程序开始介绍,但又 ...
- 9月《中国数据库行业分析报告》已发布,47页干货带你详览 MySQL 崛起之路!
为了帮助大家及时了解中国数据库行业发展现状.梳理当前数据库市场环境和产品生态等情况,从2022年4月起,墨天轮社区行业分析研究团队出品将持续每月为大家推出最新<中国数据库行业分析报告>,持 ...