精读《手写 JSON Parser》

1 引言

JSON.parse 是浏览器内置的 API,但如果面试官让你实现一个怎么办?好在有人已经帮忙做了这件事,本周我们一起精读这篇 JSON Parser with Javascript 文章吧,再温习一遍大学时编译原理相关知识。

2 概述 & 精读

要解析 JSON 首先要理解语法概念,之前的 精读《手写 SQL 编译器 - 语法分析》 系列也有介绍过,不过本文介绍的更形象,看下面这个语法图:

这是关于 Object 类型的语法描述图,从左向右看,根据箭头指向只要能走出这个迷宫就属于正确语法。

比如第一行 {whitespace} 表示 { } 属于合法的 JSON 语法。

再比如观察向下的一条最长路线:{whitespacestringwhitespace:value} 表示 { string : value } 属于合法的 JSON 语法。

你可能会问,双引号去哪儿了?这就是语法树最核心的概念了,这张图是关于 Object 类型的 产生式,同理还有 string、value 的产生式,产生式中可以嵌套其他产生式,甚至形成环路,以此拥有描述纷繁多变语法的能力。

最后我们再看一个环路,即 {whitespacestring,whitespacestring,},我们发现,只要不走回头路,这条路是可以一直 “绕圈” 下去的,因此 Object 类型拥有了任意数量子字段的能力,只是每形成一个子字段,必须经过 , 号分割。

实现 Parser

首先实现一个基本结构:

function fakeParseJSON(str) {let i = 0;// TODO
}

i 表示访问字符的下标,当 i 走到字符串结尾表示遍历结束。

然后是下一步,用几个函数描述解析语法的过程:

function fakeParseJSON(str) {let i = 0;function parseObject() {if (str[i] === '{') {i++;skipWhitespace();// if it is not '}',// we take the path of string -> whitespace -> ':' -> value -> ...while (str[i] !== '}') {const key = parseString();skipWhitespace();eatColon();const value = parseValue();}}}
}

其中 skipWhitespace 表示匹配并跳过空格,所谓匹配意味着匹配成功,此时 i 下标可以继续后移,否则匹配失败。下一步则判断如果 i 不是结束标志 },则按照 parseString 匹配字符串 → skipWhitespace 跳过空格 → eatColon 吃掉冒号 → parseValue 匹配值,这个链路循环。其中吃掉冒号表示 “匹配冒号但不会产生任何结果,所以就像吃掉了一样”,吃这个动作还可以用在其他场景,比如吃掉尾分号。

对于看到这儿的小伙伴,笔者要友情提示一下,原文的思路是一种定制语法解析思路,无论是 eatColon 还是 parseValue 都仅具备解析 JSON 的通用性,但不具备解析任意语法的通用性。如果你想做一个具备解析任何通用语法的解析器,读入的内容应该是语法描述,处理方式必须更加通用,如果感兴趣可以阅读 精读《手写 SQL 编译器 - 语法分析》 系列文章了解更多。

由于 Object 第一个元素前面不允许加逗号,因此可以利用 initial 做一个初始化判定,在初始时机不会吃掉逗号:

function fakeParseJSON(str) {let i = 0;function parseObject() {if (str[i] === '{') {i++;skipWhitespace();let initial = true;// if it is not '}',// we take the path of string -> whitespace -> ':' -> value -> ...while (str[i] !== '}') {if (!initial) {eatComma();skipWhitespace();}const key = parseString();skipWhitespace();eatColon();const value = parseValue();initial = false;}// move to the next character of '}'i++;}}
}

那么当第一个子元素前面存在逗号时,由于没有 “吃掉逗号” 这个功能,所以读到逗号会报错,语法解析提前结束。

吃逗号和吃冒号的代码都非常简单,即判断当前字符串必须是 “要吃的那个元素”,并且在吃掉后将 i 下标自增 1:

function fakeParseJSON(str) {// ...function eatComma() {if (str[i] !== ',') {throw new Error('Expected ",".');}i++;}function eatColon() {if (str[i] !== ':') {throw new Error('Expected ":".');}i++;}
}

在有了基本判定功能后,fakeParseJSON 需要返回 Object,因此我们只需在每个循环中对 Object 赋值,最后一并 return 即可:

function fakeParseJSON(str) {let i = 0;function parseObject() {if (str[i] === '{') {i++;skipWhitespace();const result = {};let initial = true;// if it is not '}',// we take the path of string -> whitespace -> ':' -> value -> ...while (str[i] !== '}') {if (!initial) {eatComma();skipWhitespace();}const key = parseString();skipWhitespace();eatColon();const value = parseValue();result[key] = value;initial = false;}// move to the next character of '}'i++;return result;}}
}

解析 Object 的代码就完成了。

接着试着解析 Array,下面是 Array 的语法图:

我们只需要吃逗号和 parseValue 即可:

function fakeParseJSON(str) {// ...function parseArray() {if (str[i] === '[') {i++;skipWhitespace();const result = [];let initial = true;while (str[i] !== ']') {if (!initial) {eatComma();}const value = parseValue();result.push(value);initial = false;}// move to the next character of ']'i++;return result;}}
}

接下来到了有趣的 value 语法图,可以看到 value 是许多种基础类型的 “或” 关系组成的:

我们只需要继续拆解分析即可:

function fakeParseJSON(str) {// ...function parseValue() {skipWhitespace();const value =parseString() ??parseNumber() ??parseObject() ??parseArray() ??parseKeyword('true', true) ??parseKeyword('false', false) ??parseKeyword('null', null);skipWhitespace();return value;}
}

其中 parseKeyword 函数用来解析一些保留关键字,比如将 "true" 解析成布尔类型 true

function fakeParseJSON(str) {// ...function parseKeyword(name, value) {if (str.slice(i, i + name.length) === name) {i += name.length;return value;}}
}

如上所示,只要在 name 与对应字符相等时,返回第二个传入参数即可。

处理异常输入

一个完整的语法解析功能需要包含错误处理,错误的情况主要分两种:

  1. 非法字符。
  2. 非正常结尾。

原文提到的 JSON 错误提示优化非常棒,想想你在开发中突然看到下面的提示,是不是很蒙圈:

Unexpected token "a"

既然我们是自己写的 JSON 解析器,就可以进行更友好的异常提示,比如:

// show
{ "b"a^
JSON_ERROR_001 Unexpected token "a".
Expecting a ":" over here, eg:
{ "b": "bar" }^
You can learn more about valid JSON string in http://goo.gl/xxxxx

更多 Demo 可以查看 原文。

3 总结

这篇文章通过一个具体的例子解释如何做语法分析,对于词法解析入门非常直观,如果你想更深入理解语法解析,或者写一个通用语法解析器,可以阅读语法解析系列入门文章,笔者通过实际例子带你一步一步做一个完备的词法解析工具!

语法解析入门系列文章,建议阅读顺序:

  • 精读《手写 SQL 编译器 - 词法分析》
  • 精读《手写 SQL 编译器 - 文法介绍》
  • 精读《手写 SQL 编译器 - 语法分析》
  • 精读《手写 SQL 编译器 - 回溯》
  • 精读《手写 SQL 编译器 - 语法树》
  • 精读《手写 SQL 编译器 - 错误提示》
  • 精读《手写 SQL 编译器 - 性能优化之缓存》
  • 精读《手写 SQL 编译器 - 智能提示》

syntax-parser 这个零依赖的通用语法解析库就是根据上述文章一步一步完成的,看完了上面文章,就彻底理解了这个库的源码。

讨论地址是:精读《手写 JSON Parser》 · Issue #233 · dt-fe/weekly

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/749853.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【机器学习】分类模型的评价方法

🌻个人主页:相洋同学 🥇学习在于行动、总结和坚持,共勉! #学习笔记# 目录 一、混淆矩阵(Confusion Matrix) 二、评估指标(Evaluation metrics) 1.正确率(accuracy) …

R统计学3 - 数据分析入门问题41-60

往期R统计学文章: R统计学1 - 基础操作入门问题1-20 R统计学2 - 数据分析入门问题21-40 41. R 语言如何做双坐标图? # 创建模拟数据 year <- 2014:2024 gdp <- data.frame(year, GDP = sort(rnorm(11, 1000, 100))) ur <- data.frame(year, UR = rnorm(11, 5, 1…

计算机网络(7)----应用层

目录 一.应用层的基本概念 1.应用层的基本概述 2.网络应用模型 &#xff08;1&#xff09;客户/服务器模型 &#xff08;2&#xff09;P2P模型 二.应用程序相关 1.DNS系统 &#xff08;1&#xff09;域名与域名服务器 &#xff08;2&#xff09;域名解析过程&#xff…

2024 第一届VCTF 纳新赛 Web方向 题解WP

hackjs 题目描述&#xff1a;A baby oldjs, just warm up. 附件给源码 const express require(express) const fs require(fs) var bodyParser require(body-parser); const app express() app.use(bodyParser.urlencoded({extended: true })); app.use(bodyParser.json…

CI/CD实战-git工具使用 1

版本控制系统 本地版本控制系统 集中化的版本控制系统 分布式版本控制系统 git官网文档&#xff1a;https://git-scm.com/book/zh/v2 Git 有三种状态&#xff1a;已提交&#xff08;committed&#xff09;、已修改&#xff08;modified&#xff09; 和 已暂存&#xff08;sta…

嵌入式硬件设计(一)|利用 NodeMCU-ESP8266 开发板和继电器结合APP“点灯•blinker”制作Wi-Fi智能开关(附有关硬件详细资料)

概述 本文主要讲述利用 NodeMCU-ESP8266 开发板和继电器通过手机 APP “ 点灯 • Blinker ” 制作一款能够由手机控制的WiFi 智能开关&#xff0c;从而实现智能物联。NodeMCU 是基于 Lua 的开源固件&#xff0c;ESP8266-NodeMCU是一个开源硬件开发板&#xff0c;支持WiFi功能&a…

OpenCV4.9.0开源计算机视觉库在 Linux 中安装

返回目录&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV 环境变量参考 下一篇&#xff1a;将OpenCV与gcc和CMake结合使用 引言&#xff1a; OpenCV是一个开源的计算机视觉库&#xff0c;由英特尔公司所赞助。它是一个跨…

深度学习-基于机器学习的情绪分析研究

概要 互联网技术的迅速发展使得社交平台逐渐成为热点事件中社会情感的枢纽。社会热点事件的舆论监管的其中一个重要环节就是能够准确分析民众的社会情绪。本文旨在探索可以基于文本大数据彻底分析民众对热点事件的社会情绪的模型和方法。先是从社交平台上借助文本大数据、对数据…

(一)Neo4j下载安装以及初次使用

&#xff08;一&#xff09;下载 官网地址&#xff1a;Neo4j Graph Database & AnamConnect data as its stored with Neo4j. Perform powerful, complex queries at scale and speed with our graph data platform.https://neo4j.com/ &#xff08;二&#xff09;安装并配…

ideaSSM失物招领管理系统网页模式开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 idea ssm 失物招领管理系统是一套完善的完整信息管理系统&#xff0c;结合SSM框架完成本系统SpringMVC spring mybatis &#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数…

YOLOv8训练好模型后,追加轮数继续训练、或者提前终止训练,缩减训练轮数

一、前言 而且此教程适用的情况是你已经训练好了此模型&#xff0c;想继续追加一些轮数。比如训练进度是120/120&#xff0c;已经完成了&#xff0c;继续追加10轮&#xff0c;或者你原先定的是200轮&#xff0c;希望缩减到150轮&#xff0c;可以使用我说的这个方法。为什么缩减…

深度学习-2.7 机器学习目标与模型评估方法

文章目录 深度学习目标与模型评估方法1. 深度学习目标与模型评估方法2. 手动实现训练集和测试集切分3. Dataset和DataLoader基本使用方法与数据集切分函数1.Dataset和DataLoader的基本使用方法2.建模及评估过程 4. 实用函数补充 深度学习目标与模型评估方法 1. 深度学习目标与…

LeetCode 7 / 100

哈希表、双指针 哈希表两数之和字母异位词分组最长连续序列 双指针移动零盛最多水的容器三数之和接雨水 LeetCode 1.两数之和 LeetCode 49. 字母异位词分组 LeetCode 128. 最长连续序列 LeetCode [283. 移动零](https://leetcode.cn/problems/move-zeroes/?envTypestudy-plan-…

Spring Cloud Alibaba微服务从入门到进阶(五)(负载均衡-Ribbon)

负载均衡有两种形式&#xff0c;服务器端负载均衡/客户端负载均衡 1、服务器端负载均衡 因为Nginx是部署在服务器端的&#xff0c;所以用Nginx实现的负载均衡被称为服务器端负载均衡 2、客户端负载均衡 手写一个客户端侧负载均衡器 使用Ribbon实现负载均衡 Ribbon是Netflix…

sparksession对象简介

什么是sparksession对象 spark2.0之后&#xff0c;sparksession对象是spark编码的统一入口对象&#xff0c;通常我们在rdd编程时&#xff0c;需要SparkContext对象作为RDD编程入口&#xff0c;但sparksession对象既可以作为RDD编程对象入口&#xff0c;在sparkcore编程中可以通…

牛客网-SQL大厂面试题-2.平均播放进度大于60%的视频类别

题目&#xff1a;平均播放进度大于60%的视频类别 DROP TABLE IF EXISTS tb_user_video_log, tb_video_info; CREATE TABLE tb_user_video_log (id INT PRIMARY KEY AUTO_INCREMENT COMMENT 自增ID,uid INT NOT NULL COMMENT 用户ID,video_id INT NOT NULL COMMENT 视频ID,start…

macbook删除软件只需几次点击即可彻底完成?macbook删除软件没有叉 苹果笔记本MacBook电脑怎么卸载软件? cleanmymac x怎么卸载

在MacBook的使用过程中&#xff0c;软件安装和卸载是我们经常需要进行的操作。然而&#xff0c;不少用户在尝试删除不再需要的软件时&#xff0c;常常发现这个过程既复杂又耗时。尽管MacOS提供了一些基本的macbook删除软件方法&#xff0c;但很多时候这些方法并不能彻底卸载软件…

Learn OpenGL 15 面剔除

面剔除 尝试在脑子中想象一个3D立方体&#xff0c;数数你从任意方向最多能同时看到几个面。如果你的想象力不是过于丰富了&#xff0c;你应该能得出最大的面数是3。你可以从任意位置和任意方向看向这个球体&#xff0c;但你永远不能看到3个以上的面。所以我们为什么要浪费时间…

html编辑器

HTML 编辑器推荐 html可以使用记事本编辑 但是更建议使用专业的 HTML 编辑器来编辑 HTML&#xff0c;我在这里给大家推荐几款常用的编辑器&#xff1a; VS Code&#xff1a;https://code.visualstudio.com/WebStorm: https://www.jetbrains.com/webstorm/Notepad: https://no…

基于Matlab的车牌识别算法,Matlab实现

博主简介&#xff1a; 专注、专一于Matlab图像处理学习、交流&#xff0c;matlab图像代码代做/项目合作可以联系&#xff08;QQ:3249726188&#xff09; 个人主页&#xff1a;Matlab_ImagePro-CSDN博客 原则&#xff1a;代码均由本人编写完成&#xff0c;非中介&#xff0c;提供…