处理后台返回JSON数据number类型精度丢失,手写JSON字符串转JSON对象

起因

后台返回的JSON数据中,部分ID使用了一长串数字作为ID使用,但由于JS的number会伴随精度丢失的问题,很有可能你的9999999999999999读出来就变成了10000000000000000了,后续数据交互若需要此值,则每次得到的始终都不会是期望的值

解决办法

  • 后台处理的方式,将可能的值转成字符串返回,这种方式比较简单,只需要后端支持一下即可(想要正常的计算,转为bigInt即可)
  • 前端处理的方式
    1. 使用正则,将请求获取到的原始数据先行处理一遍,给大于精度临界值的值加上双引号,然后再交给JSON.parse处理
    2. 自行解析JSON,这种处理方式可能会伴随部分性能的损失,不推荐(然鹅,我还是用了这种)。

手写JSON解析器

我的思路是,以逐字符读取的方式,将每个字符一一解析,自行判断类型和获取值,最终得到完整的JSON对象,再添加一个附加功能,能够hook到数据处理的关键步骤中,以达到自行对数据预处理,按着如上思路,有了以下代码:

enum JsonType {
  String,
  Boolean,
  Number,
  Null,
  JsonObject,
  JsonArray,
}

interface Result {
  key: string;
  value: any;
  resource: string;
}

export class DGson {
  jsonStr = '';
  hooks: any;

  /** char-index */
  charIndex = 0;

  constructor(jsonStr: string = '', hooks?: (result: Result) => any) {
    this.jsonStr = jsonStr;
    this.hooks = hooks;
    this.jsonStrPreprocessing();
    return this.getValue().value;
  }

  jsonStrPreprocessing() {
    this.jsonStr = this.jsonStr
      .replace(/\\n/g, '\n')
      .replace(/\\r/g, '\r')
      .replace(/\\t/g, '\t')
      .replace(/\\b/g, '\b')
      .replace(/\\f/g, '\f')
      .replace(/\\\\/g, '\\');
  }

  /** factory */
  readCharFactory(): string {
    let firstChar;
    if (this.jsonStr.length > this.charIndex) {
      firstChar = this.jsonStr[this.charIndex];
      this.charIndex += 1;
    }
    return firstChar as string; // 强制声明为string,原因是正则匹配类型检查不允许undefined
  }

  /** 工厂回流 */
  backFlowReadChar(n: number) {
    this.charIndex -= n;
  }

  readCharHeadNotEmpty() {
    let result = this.readCharFactory();

    while (/\s/.test(result)) {
      // 忽略空值,继续往下读
      result = this.readCharFactory();
    }

    return result;
  }

  getValue() {
    // const firstChar = this.readCharFactory();
    let result: {
      value: any;
      resource: string;
      key: string;
    } = {
      value: '',
      resource: '',
      key: '',
    };
    let nextFirstStr = this.readCharHeadNotEmpty();
    const type = this.getType(nextFirstStr);

    if (type === JsonType.JsonObject) {
      result = {
        key: '',
        value: {},
        resource: '',
      };
      let isReadNodeDone = false;
      while (!isReadNodeDone) {
        nextFirstStr = this.readCharHeadNotEmpty();
        if (nextFirstStr === '"') {
          // 发现双引号代表找到key值的开头了
          const key = this.getKey();
          let keyAfterChar = this.readCharHeadNotEmpty();
          if (keyAfterChar !== ':') {
            // key后面需要有 : 符号,否则视为非法
            this.throwNoSuchChar(':');
          }
          const value = this.getValue();
          let valueAfterChar = this.readCharHeadNotEmpty();
          if (/[,}]/.test(valueAfterChar)) {
            if (valueAfterChar === '}') {
              // 如果发现结尾了,需要停止循环,代表已经读到尽头
              isReadNodeDone = true;
              // this.backFlowReadChar(1);
            } else {
              // nextFirstStr = this.readCharFactory();
            }
          } else {
            this.throwNoEndSign(`,' or '}`);
          }

          result.value[key] = this.hooks ? this.hooks(value) : value.value;
        } else if (nextFirstStr === '}') {
          isReadNodeDone = true;
        } else {
          this.throwNoEndSign('}');
        }
      }
    } else if (type === JsonType.JsonArray) {
      result = {
        key: '',
        value: [],
        resource: '',
      };
      nextFirstStr = this.readCharHeadNotEmpty();
      if (nextFirstStr !== ']') {
        this.backFlowReadChar(1);
        let isReadNodeDone = false;
        while (!isReadNodeDone) {
          const value = this.getValue();
          let valueAfterChar = this.readCharFactory();
          while (/\s/.test(valueAfterChar)) {
            valueAfterChar = this.readCharFactory();
          }
          if (/[,\]]/.test(valueAfterChar)) {
            if (valueAfterChar === ']') {
              isReadNodeDone = true;
            }
          } else {
            this.throwNoEndSign(`,' or ']`);
          }
          
          if(this.hooks){
            result.value.push(this.hooks(value));
          }
          else{
            result.value.push(value.value);
          }
        }
      }
    } else if (type === JsonType.String) {
      const v = this.getStringValue();
      result = {
        key: '',
        value: v,
        resource: v,
      };
    } else if (type === JsonType.Number) {
      const v = this.getNumberValue();
      result = {
        key: '',
        value: parseFloat(v),
        resource: v,
      };
    } else if (type === JsonType.Boolean) {
      const v = this.getBooleanValue();
      result = {
        key: '',
        value: /true/i.test(v),
        resource: v,
      };
    } else if (type === JsonType.Null) {
      const v = this.getNullValue();
      result = {
        key: '',
        value: null,
        resource: v,
      };
    } else {
      this.throwError(`This value cannot be resolved`);
    }

    return result;
  }

  getKey() {
    let result = this.getStringValue();
    if (result === '') {
      // 键不允许为空
      this.throwError(`Key is not allowed to be empty`);
    }
    return result;
  }

  getObjectValue() {}

  getStringValue() {
    let result = '';
    let nextFirstStr = this.readCharFactory();
    while (nextFirstStr !== '"') {
      if (nextFirstStr === undefined) {
        this.throwNoEndSign(`"`);
      }

      if (nextFirstStr === '\\') {
        // 发现反斜杠,如果反斜杠后面是双引号,则表明下一个双引号是需要录入的,并且不算结束符号,反之则将反斜杠算作常规字符
        nextFirstStr = this.readCharFactory();
        if (nextFirstStr === '"') {
          result += nextFirstStr;
        } else if (nextFirstStr !== undefined) {
          result += '\\' + nextFirstStr;
        } else {
          this.throwNoEndSign('"');
        }
      } else {
        result += nextFirstStr;
      }

      nextFirstStr = this.readCharFactory();
    }

    return result;
  }

  getNumberValue() {
    const lastStr = this.jsonStr[this.charIndex - 1]; // 获取上一个值,因为这是不可缺少的一部分
    let result = '';
    let nextFirstStr = lastStr;

    while (/[0-9\.e\-\+]/i.test(nextFirstStr)) {
      const lastStr = this.jsonStr[this.charIndex - 2];
      if (/[0-9]/.test(nextFirstStr)) {
        result += nextFirstStr;
        nextFirstStr = this.readCharFactory();
      } else if (nextFirstStr === '.') {
        // 如果出现小数点,需要检查前面是否有过小数点,并且需要检查上一个字符是否是数字
        if (!/\./.test(result) && /[0-9]/.test(lastStr)) {
          result += nextFirstStr;
          nextFirstStr = this.readCharFactory();
          if (/[0-9]/.test(nextFirstStr)) {
            result += nextFirstStr;
          } else {
            this.throwError(`Floating point values are incomplete`);
          }
        }
        else {
          this.throwError(`Point is error`);
        }
      } else if (/-/.test(nextFirstStr)) {
        if (result.length > 0 && !/e/i.test(lastStr)) {
          // 如果前面是e,则表示可能是科学计数法
          if (/e/.test(lastStr)) {
            result += nextFirstStr;
            nextFirstStr = this.readCharFactory();
            if (/[0-9]/.test(nextFirstStr)) {
              // 科学计数法符号e后面必须跟随数字,否则就是不完整的错误格式
              result += nextFirstStr;
            } else {
              this.throwError(`The expression of scientific counting method is incomplete`);
            }
          } else {
            this.throwError(`The symbol "-" can only appear after the beginning or 'e'`);
          }
        } else {
          result += nextFirstStr;
        }
      } else if (/e/i.test(nextFirstStr)) {
        if (/e/i.test(result)) {
          this.throwError(`
            It's impossible to have two characters e`);
        } else {
          result += nextFirstStr;
        }
      } else if (/\+/.test(nextFirstStr)) {
        if (result.length > 0 && /e/i.test(lastStr)) {
          // 如果前面是e,则表示可能是科学计数法
          result += nextFirstStr;
          nextFirstStr = this.readCharFactory();
          if (/[0-9]/.test(nextFirstStr)) {
            result += nextFirstStr;
          } else {
            this.throwError(`The expression of scientific counting method is incomplete`);
          }
        } else {
          this.throwError(`Can't start with an '+' sign`);
        }
      } else {
        // nextFirstStr = this.readCharFactory();
      }
    }

    this.backFlowReadChar(1); // Number类型不定长度,获取到最后会将下一个字符吞并,所以需要回流
    return result;
  }

  getNullValue() {
    const lastStr = this.jsonStr[this.charIndex - 1]; // 获取上一个值,因为这是不可缺少的一部分
    let result = lastStr;

    for (let i = 0; i < 3; i++) {
      result += this.readCharFactory();
    }

    if (!/null/i.test(result)) {
      this.throwError(`Value '${result}' is not 'Null' type`);
    }

    return result;
  }

  getBooleanValue() {
    const lastStr = this.jsonStr[this.charIndex - 1]; // 获取上一个值,因为这是不可缺少的一部分
    let result = lastStr;

    for (let i = 0; i < 3; i++) {
      result += this.readCharFactory();
    }

    if (/fals/i.test(result)) {
      result += this.readCharFactory();
    }

    if (!/true|false/i.test(result)) {
      this.throwError(`Value '${result}' is not 'Boolean' type`);
    }

    return result;
  }

  getType(aChar: string = '') {
    let result;
    if (aChar === '{') {
      result = JsonType.JsonObject;
    } else if (aChar === '[') {
      result = JsonType.JsonArray;
    } else if (aChar === '"') {
      result = JsonType.String;
    } else if (aChar === '-' || /[0-9]/.test(aChar)) {
      result = JsonType.Number;
    } else if (/[tf]/i.test(aChar)) {
      result = JsonType.Boolean;
    } else if (/n/i.test(aChar)) {
      result = JsonType.Null;
    } else {
      this.throwError(`No matching type was found`);
    }

    return result;
  }

  throwError(e: string) {
    throw `DGson Exception: ${e}, at position ${this.charIndex}`;
  }

  /**
   * 没有找到对应结尾字符的异常
   * @param aChar 该字符应该是char类型
   */
  throwNoEndSign(aChar: string) {
    this.throwError(`No end sign '${aChar}' was found`);
  }

  /**
   * 没有找到某个字符异常
   */
  throwNoSuchChar(aChar: string) {
    this.throwError(`No such char is '${aChar}'`);
  }
}

手写解析器的性能

自行测试的时候发现,比起JSON.parse,性能总是会慢30毫秒,不过还在我接受范围之中,所以就忽略啦。代码中还有很多地方应该是可以调整优化下来提升性能的,后续有时间再做吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 一. XML数据交换格式 XML数据交换格式是一种自描述的数据交互格式,虽然XML数据格式不如JSON "轻便",...
    __season____阅读 2,486评论 0 7
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,421评论 1 45
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • JSON JSON和XML都是需要解析的 JSON是一种轻量级的数据格式,一般用于数据交互服务器返回给客户端的数据...
    JonesCxy阅读 1,842评论 2 10
  • 前言: 最近也是期末了,有好多好多文档和实验报告要交,所以都没啥时间写文,这段时间清闲了,来补一下之前学习时遗漏的...
    我没有三颗心脏阅读 3,154评论 0 12