如何手写一个JSON解析器?

滞空容器
• 阅读 3715

前言

前一段时间在工作的时候,遇到了如下的问题。后端传给我的JSON,其中id字段使用的number的格式,但是id的大小超过了2^53 - 1 ~ 2^53 - 1的范围。导致JSON.parse解析的过程中数字溢出。后端又不愿意修改接口。最后使用了json-bigint这个库解析JSON,替代了JSON.parse。将大数直接解析为字符串。

我很好奇,json-bigint的工作原理,于是阅读了json-bigint的源码,发现原理并不复杂,于是写下这篇文章,供大家参考。

JsonBigint的使用

首先介绍下JsonBigint的基本使用

// 安装
npm install json-bigint

import JSONbig from 'json-bigint';

const json = '{ "value" : 9223372036854775807, "v2": 123 }';
// 9223372036854776000 发生了溢出
JSON.parse(json).value 
// '9223372036854775807' 将大数转为了字符串
JSONbig.parse(json).value

JsonBigint的原理

JsonBigint的原理,主要是逐一解析JSON中的每一个字符,并根据不同的规则将value,解析为object,array,number,,string,boolean值等。

JsonBigint的目录结构

如何手写一个JSON解析器?

JsonBigint主要暴露了两个API, JSONbig.parseJSONbig.stringify,我们主要看下 JSONbig.parse 方法。JSONbig.parse方法的代码,主要在parse.js文件中。

index.js

通过入口文件index.js可得知parse函数,自身会返回一个函数。并将返回函数暴露到API上。

var json_parse = require('./lib/parse.js');

// 调用json_parse,并将返回值暴露给parse属性
module.exports.parse = json_parse();

parse.js

parse.js中是JSONbig.parse的核心代码所在的位置,我删除了部分对特殊情况的判断,只保留了源码的核心部分,以方便大家理解。接下来,我们就来解读下核心源码吧。

入口函数

先来解释下,入口函数的参数和变量

  • source参数, 是我们需要解析的json字符串
  • at,索引。我们需要从头到尾逐个字符解析json,所以索引初始等于0。
  • ch, 是当前正则解析的字符串,默认等于空字符串。
  • text,source参数的副本
function (source) {
    var result;
    text = source + '';
    at = 0;
    ch = ' ';
    result = value();
    white();
    if (ch) {
        error('Syntax error');
    }
    return result;
};

接着入口参数调用了value函数,value函数会开始解析text变量,并返回解析后内容。在解析完成后,如果还有多余的非空格字符没有被解析,说明json是不合法的。抛出错误,否则返回value返回的结果。

value

因为json在没有解析前,都是字符串的格式,所以我们可以根据字符串的第一个字符,判读json到底是什么类型的。

  • 如果是{, 说明json解析后应该是object
  • 如果是[, 说明json解析后应该是array
  • 如果是",说明解析后应该是字符串(JSON的标准是使用“)
  • 如果是-,说明是数字,只不过是负数
  • 如果开头的字符串是0~9内,说明是字符串。如果不是,按照boolean或者null处理
value = function () {
    white();
    switch (ch) {
      case '{':
        return object();
      case '[':
        return array();
      case '"':
        return string();
      case '-':
        return number();
      default:
        return ch >= '0' && ch <= '9' ? number() : word();
    }
  };

value函数中,第一句话调用的是white函数,white函数是做什么的呢?

white函数会逐字读取json字符串(并排除空格字符),并将读取的字符串赋予ch变量。我们根据ch变量,并结合上面的规则,开始使用不同函数开始解析

white & next

white函数的作用主要是用来,删除json中多余的空格字符。white函数中会开启一个while循环,如果ch是空格字符串,ch && ch <= ' '的循环条件,就会返回true,while循环就会继续下去,直到ch不在是空格字符串。

// white
white = function () {
    while (ch && ch <= ' ') {
        next();
    }
},

next函数,会根据索引at,取出json字符串中对应位置的字符。at索引会自增。next函数还有一个作用是判读参数是否和ch相等,如果不相等会抛出错误。

// next函数
next = function (c) {
    if (c && c !== ch) {
      error("Expected '" + c + "' instead of '" + ch + "'");
    }
    ch = text.charAt(at);
    at += 1;
    return ch;
},

object

我们首先看下object类型的json张什么样子"{"key": value}"或"{}"。我们可以看到第一个字符是"{",。而第二个非空字符必须且应该是", 或者是}。否则json就是不合法的

如果是}, 说明是json一个空对象,直接返回空对象就行。

如果是", 说明json是有属性的。"和"之间的,应该是一个字符串,这个字符串是object的第一个属性。

如果这两个都不是,说明这个object json是非法的。需要抛出错误。

对于value,value的类型可能是objcet,array,boolean,string,number是不确定的

object = function () {
    var key,
    object = Object.create(null);

    if (ch === '{') {
      // 判读ch是否等于"{", 并读取第二个字符
      next('{');
      // 如果第二个字符是空格字符,white函数会尝试读取到第一个非空格的字符
      // 并将第二个非空格字符赋值给ch
      white();
      // 如果第二个字符是"}",说明这个object是一个空对象,直接返回空对象即可
      if (ch === '}') {
        next('}');
        return object;
      }
      // 如果不是 }, 并且json合法的情况下,第二个非空格字符应该是"
      // 在{ 和 冒号 之间,在json是合法情况下,是object的key,格式为"key"
      while (ch) {
        // string读取两个""之间的内容
        key = string();
        // 读取完key后,接着向后读取
        white();
        // key之后的第一个非空字符串,应该是:, 否则就是不合法的。
        next(':');
        // : 之后就应该是key对应的属性值
        // 属性值的类型不固定,我们还需要借助value函数,尝试判断中属性的类型,做不同的处理
        // value会返回解析后的属性值,并返回。
        // 我们将key和value添加到空对象上
        object[key] = value();
        // 在获取完value之后,接着向后读取
        // 如果读取到},说明ojbect解析完成,返回object即可
        white();
        if (ch === '}') {
          next('}');
          return object;
        }
        // 如果读取到,说明还有其他属性,进入下一次的迭代
        next(',');
        white();
      }
    }
    error('Bad object');
  };

string

在json中字符串内容必须使用两个双引号抱起来,举一个例子{"key":"value"}。

string函数会读取两个双引号之间的内容,并返回。如果读取到最后,没有读取到下一个", 说明字符串没有闭合,不合法抛出错误


string = function () {
    var string = '';
    // 如果是ch是"
    // while循环会一直尝试读取到第二个"
    // 并且将两个"之间的内容赋予string
    // 最后将string返回
    if (ch === '"') {
      var startAt = at;
      while (next()) {
        if (ch === '"') {
          if (at - 1 > startAt) string += text.substring(startAt, at - 1);
          next();
          return string;
        }
      }
    }
    error('Bad string');
  },

array

我们首先看下array类型的json张什么样子"[value, value]", 在第一个字符[之后。要么是数组的第一个内容,要么是 ]。

如果是], 说明数组是一个空数组。返回空数组即可。

如果不是,说明数组不是空数组。由于数组中内容的类型不固定,我们还需要借助value函数,尝试判断数组中内容的类型。然后做不同的处理。直到读取到]字符,然后返回整个数组。

array = function () {

    var array = [];
    // 如果是数组类型,第一个字符必须是[, 如果不是说明是不合法的array
    if (ch === '[') {
      next('[');
      // 尝试读取到第二个非空格的字符串
      white();
      // 如果第二个非空格的字符是], 说明是空字符串,直接返回空数组
      if (ch === ']') {
        next(']');
        return array;
      }
      // 如果第二个非空格字符,不是]
      // 由于数组中的内容的类型,不确定,我们需要使用value函数,读取内容并返回。
      while (ch) {
        array.push(value());
        white();
        // 在读取完第一个内容,如果之后的字符是], 说明数组读取完毕,返回数组即可
        if (ch === ']') {
          next(']');
          return array;
        }
        // 在读取完第一个内容,如果之后的字符是逗号,说明数组还有其他内容,进入下一次循环
        next(',');
        white();
      }
    }
    error('Bad array');
  },

number


number = function () {
    var number,
      string = '';

    // 如果第一个字符串是 - ,说明number可能会是负数,继续向后查找
    if (ch === '-') {
      string = '-';
      next('-');
    }

    // 如果是0~9之间的字符,string进行累加
    while (ch >= '0' && ch <= '9') {
      string += ch;
      next();
    }

    // 如果是小数点的处理
    if (ch === '.') {
      string += '.';
      while (next() && ch >= '0' && ch <= '9') {
        string += ch;
      }
    }

    // 如果是科学计数法的处理
    if (ch === 'e' || ch === 'E') {
      string += ch;
      next();
      if (ch === '-' || ch === '+') {
        string += ch;
        next();
      }
      while (ch >= '0' && ch <= '9') {
        string += ch;
        next();
      }
    }
    // 将string转为数字,并将数字赋予number变量
    number = +string;

    // 如果number是nan,或者政府无穷大isFinite返回false
    // 比如,isFinite('-'),返回false
    // 如果返回false,抛出错误
    if (!isFinite(number)) {
      error('Bad number');
    } else {
      // 如果string的长度大于15, 说明number的大小已经溢出,我们返回字符串
      if (string.length > 15)
        return string
      else
       // 如果string的长度小于15, 我们返回数字类型
        return number
    }

word

word函数主要是用来处理boolean类型和null的。

我们首先看下,boolean类型和null在json中张什么样。"{"key1":true,"key2":false,"key3":null}", 在json中它们就是没用使用双引号包裹的普通字符。

word = function () {
    switch (ch) {
      // 如果第一个字符是t,那么接下来的字符必须依次是t r u e,否则会抛出错误
      case 't':
        next('t');
        next('r');
        next('u');
        next('e');
        // 返回true
        return true;
      // 如果第一个字符是f,那么接下来的字符必须依次是f a l s e,否则会抛出错误
      case 'f':
        next('f');
        next('a');
        next('l');
        next('s');
        next('e');
        // 返回false
        return false;
      // 如果第一个字符是n,那么接下来的字符必须依次是n  u l l,否则会抛出错误
      case 'n':
        next('n');
        next('u');
        next('l');
        next('l');
        // 返回null
        return null;
    }
    error("Unexpected '" + ch + "'");
  },
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究