如何手写实现 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打交道时更加得心应手.由于个人水平有限 ...
随机推荐
- HLK-RM60 + openwrt调试
1. 简介 HLK-RM60官网 https://www.hlktech.com/en/Goods-176.html 采用联发科SOC, MT7621/MT7905/MT7975 实际上采购的是MT7 ...
- uni-app 小程序用户信息之头像昵称填写
小程序获取用户头像昵称,微信又叒做妖,废除之前的接口,改成了头像昵称填写 通知:微信小程序端基础库2.27.1及以上版本,wx.getUserProfile 接口被收回,详见<小程序用户头像昵称 ...
- Python新手爬虫二:爬取搜狗图片(动态)
经过上一期爬取豆瓣影评成功后,感觉爬虫还不错,于是想爬点图片来玩玩... 搜狗图片地址:https://pic.sogou.com/?from=category 先上最后成功的源码(在D盘下创建sou ...
- docker系列教程:docker图形化工具安装及docker系列教程总结
通过前面的学习,我们已经掌握了docker-compose容器编排及实战了.高级篇也算快完了.有没有相关,我们前面学习的时候,都是通过命令行来操作docker的,难道docker就没有图形化工具吗?答 ...
- 扩展KMP (ex_KMP)
一些约定: 字符串下标从1开始 s[1,i]表示S的第一个到第i个字符组成的字符串 解决的题型: 给你两个字符串A,B(A.size()=n,B.size()=m),求p数组 p[i]表示最大的len ...
- 计算QPS-Sentinel限流算法
sentinel 前方参考 计算QPS-Sentinel限流算法 https://www.cnblogs.com/yizhiamumu/p/16819497.html Sentinel 介绍与下载使用 ...
- ChatGPT正式登陆iOS平台
6天前,ChatGPT在美区App Store中上架了官方App,累计下载量已经突破 50 万次,OpenAI 的 ChatGPT 应用在上架之后,其热度远超必应聊天等聊天机器人,以及其它使用 GPT ...
- C# SAPX调用用户控件方法
//获得用户控件 Type pageType = ucMoneyList1.GetType(); //用户控件方法名 MethodI ...
- 全网最适合入门的面向对象编程教程:47 Python函数方法与接口-回调函数Callback
全网最适合入门的面向对象编程教程:47 Python 函数方法与接口-回调函数 Callback 摘要: 回调函数是编程中一种非常常见的模式,用于将函数作为参数传递给其他函数或方法.这种模式在 Pyt ...
- EF Core – JSON Column
前言 SQL Server 支持 JSON, 以前写过一篇介绍 SQL Server – Work with JSON. 但 EF Core 一直没有支持. 直到 EF Core 7.0 才支持. 参 ...