手写JSON.parse和JSON.stringify

简介: 搞懂了有限状态机,手写各种解析器都不在话下,主要区别也就是考虑怎么去设计各种状态流转。如果不理解有限状态机建议先去阅读前面一遍:用有限状态机实现一个简版html解析器,然后再来阅读本文就很容易理解了。

搞懂了有限状态机,手写各种解析器都不在话下,主要区别也就是考虑怎么去设计各种状态流转。
手写JSON.parse
有两种实现方式,第1种初级版没啥难度,第2种利用状态机自己去解析字符流,需要先学习下编译原理相关的知识,否则理解起来可能有点蒙。
初级版本 JSON parse
直接通过 eval 函数实现,不过注意需要在 json 字符串前后拼上括号,否则会当成代码块报错解析导致报错:

function parse(json) {
   
  const txt = '(' + json + ')'
  return eval(txt)
}

高级版本 JSON parse
主要利用有限状态机来做分词,然后再根据拿到的分词数据组装成 json 对象。
分词阶段主要时设计状态比较麻烦,刚开始可以从比较简单的状态开始,然后再一步步增加难度完善代码,想要一部到位搞好所有的状态很容易在里面绕晕。下面的版本也只考虑了一些很简单的场景,尤其嵌套数组这块直接把数组当成的一个 token,不支持内部再嵌套数组,以方便理解为主。

// 分词
function jsonTokenizer(str){
   
  // 标签开始
  const objectStartReg = /{/
  const objectEndReg = /}/
  const arrayStartReg = /\[/
  const arrayEndReg = /]/
  const numberReg = /[0-9]/
  const booleanReg = /[t|f]/
  const nullReg = /[n]/

  const keyReg = /[a-zA-Z0-9_$]/
  const quotationReg = /"/
  const commaReg = /,/
  const colonReg = /:/

    let tokens = []
    let currentToken = {
   }

  // 初始状态
  function init(e) {
   
    if (objectStartReg.test(e)) {
   
      currentToken = {
    type: 'objectStart', value: e }
            return onQuotation
        }
    if (objectEndReg.test(e)) {
   
      currentToken = {
    type: 'objectEnd', value: e }
      pushToken(currentToken)
            return init
        }
    if (arrayEndReg.test(e)) {
   
      currentToken = {
    type: 'arrayEnd', value: e }
      pushToken(currentToken)
            return init
        }

    if (commaReg.test(e)) {
   
      currentToken = {
    type: 'comma', value: e }
      pushToken(currentToken)
      return onQuotation
    }

    return init
  }

  function onQuotation(e) {
   
    if (currentToken.type === 'objectStart') {
   
      pushToken(currentToken)
      currentToken = {
    type: 'key', value: '' }
      return onKey
    }

    if (currentToken.type === 'colon') {
   
      pushToken(currentToken)
      currentToken = {
    type: 'value', value: '' }
      return onValue
    }

    if (quotationReg.test(e)) {
   
      currentToken = {
    type: 'key', value: '' }
      return onKey
    }
  }

  function onKey(e) {
   
    if (keyReg.test(e)) {
   
      currentToken.value += e
      return onKey
    }
    if (quotationReg.test(e)) {
   
      pushToken(currentToken)
      return onColon
    }
  }

  function onValue(e) {
   
    if (commaReg.test(e)) {
   
      pushToken(currentToken)
      currentToken = {
    type: 'comma', value: e }
      pushToken(currentToken)
      return onQuotation
    } else if (objectEndReg.test(e)) {
   
      pushToken(currentToken)
      currentToken = {
    type: 'objectEnd', value: e }
      pushToken(currentToken)
            return init
    } else if (objectStartReg.test(e)) {
   
      currentToken = {
    type: 'objectStart', value: e }
            return onQuotation
    } else if (arrayStartReg.test(e)) {
   
      currentToken = {
    type: 'arrayStart', value: e }
      pushToken(currentToken)
      currentToken = {
    type: 'valueArray', value: '' }
      return onAarry
    } else if (numberReg.test(e)) {
   
      currentToken = {
    type: 'valueNumber', value: e }
      return onBasicData
    } else if (booleanReg.test(e)) {
   
      currentToken = {
    type: 'valueBoolean', value: e }
      return onBasicData
    } else if (nullReg.test(e)) {
   
      currentToken = {
    type: 'valueNull', value: e }
      return onBasicData
    } else {
   
      currentToken.type = 'value'
      currentToken.value += e
      return onValue
    }
  }

  function onBasicData(e) {
   
    if (commaReg.test(e)) {
   
      pushToken(currentToken)
      currentToken = {
    type: 'comma', value: e }
      pushToken(currentToken)
      return onQuotation
    } else if (objectEndReg.test(e)) {
   
      pushToken(currentToken)
      currentToken = {
    type: 'objectEnd', value: e }
      pushToken(currentToken)
            return init
    } else {
   
      currentToken.value += e
      return onBasicData
    }
  }

  // 数组这儿比较复杂,暂时只考虑这种简单的
  function onAarry(e) {
   
    if (arrayEndReg.test(e)) {
   
      pushToken(currentToken)
      currentToken = {
    type: 'arrayEnd', value: e }
      pushToken(currentToken)
      return init
    } else {
   
      currentToken.value = (currentToken.value || '') + e
      return onAarry
    }
  }

  function onColon(e) {
   
    if (colonReg.test(e)) {
   
      currentToken = {
    type: 'colon', value: e }
      pushToken(currentToken)
      currentToken = {
    type: 'valueStart', value: '' }
      return onValue
    }
  }

  // 每次读取到完整的一个 token 后存入到数组中
    function pushToken(e) {
   
        tokens.push(e)
        currentToken = {
   }
    }

  function parse(chars){
   
    let stateMachine = init
        for (const char of chars) {
   
            stateMachine = stateMachine(char)
        }

        return tokens
    }

  return parse(str)
}

将拿到的分词数组拼成 json,主要用到了栈来缓存每次正在处理的对象,但是处理内部嵌套的引用类型值时,需要提前记住父对象的 key(子对象处理完了再赋值给父对象的key),这里我是直接每次读取到 key 时,都在当前对象上存一下 key 的值,注意需要用 symbol 类型来添加属性,否则有可能覆盖了对象里同名的属性。等设置完对应 key 的属性值后再把自己添加的这个 symbol 属性删掉。这里也可以通过一个栈来存每次读到的 key,每次要设置值时出栈就是当前要操作的 key:

// 解析
function jsonParse(tokenList) {
   
  // 用栈来存每次遇到的新对象
  let stack = []
  // 当前正在操作的对象
  let currentObj = {
   }
  // 用 symbol 类型来做属性名,防止覆盖了对象里同名的属性
  const lastKey = Symbol('lastKey')

  for (let i = 0; i < tokenList.length; i++) {
   
    const item = tokenList[i]
    if (item.type === 'objectStart') {
   
      currentObj = {
   }
      stack.push(currentObj)
    }
    if (item.type === 'objectEnd') {
   
      if (stack.length > 1) {
   
        let current = stack.pop()
        const parent = stack[stack.length - 1]

        if (parent) {
   
          const key = parent[lastKey]
          parent[key] = current

          // 设置了属性值后,删掉存的键名
          delete parent[lastKey]
        }
      }
    }
    if (item.type === 'key') {
   
      currentObj[lastKey] = item.value
    }
    if (['value', 'valueNumber', 'valueBoolean', 'valueNull', 'valueArray'].includes(item.type)) {
   
      const key = currentObj[lastKey]
      let value = item.value
      if (item.type === 'valueNumber') {
   
        value = Number(value)
      }
      if (item.type === 'valueBoolean') {
   
        value = value === 'true'
      }
      if (item.type === 'valueNull') {
   
        value = null
      }
      if (item.type === 'valueArray') {
   
        // value = value.split(',')
        value = eval('[' + value + ']')
      }

      // 非空字符串两头的引号给去掉
      const stringReg = /^"([\s\S]+)"$/
      if (stringReg.test(value)) {
   
        value = value.replace(stringReg, '$1')
      }

      currentObj[key] = value

      // 设置了属性值后,删掉存的键名
      delete currentObj[lastKey]
    }
  }

  return stack[0]
}

测试效果

const boy = {
   
  name: '周小黑',
  age: 18,
  marriage: true,
  hobby: ['吃烟', '喝酒', '烫头'],
  son: {
    nickname: '小馒头', toy: null, school: undefined }
}
const str = JSON.stringify(boy)
const arr = jsonTokenizer(str)
console.log('分词结果 -------------------')
console.log(arr)
const obj = jsonParse(arr)
console.log('解析结果 -------------------')
console.log(obj)

// // 分词结果 -------------------
// [
//   { type: 'objectStart', value: '{' },
//   { type: 'key', value: 'name' },
//   { type: 'colon', value: ':' },
//   { type: 'value', value: '"周小黑"' },
//   { type: 'comma', value: ',' },
//   { type: 'key', value: 'age' },
//   { type: 'colon', value: ':' },
//   { type: 'valueNumber', value: '18' },
//   { type: 'comma', value: ',' },
//   { type: 'key', value: 'marriage' },
//   { type: 'colon', value: ':' },
//   { type: 'valueBoolean', value: 'true' },
//   { type: 'comma', value: ',' },
//   { type: 'key', value: 'hobby' },
//   { type: 'colon', value: ':' },
//   { type: 'arrayStart', value: '[' },
//   { type: 'valueArray', value: '"吃烟","喝酒","烫头"' },
//   { type: 'arrayEnd', value: ']' },
//   { type: 'comma', value: ',' },
//   { type: 'key', value: 'son' },
//   { type: 'colon', value: ':' },
//   { type: 'objectStart', value: '{' },
//   { type: 'key', value: 'nickname' },
//   { type: 'colon', value: ':' },
//   { type: 'value', value: '"小馒头"' },
//   { type: 'comma', value: ',' },
//   { type: 'key', value: 'toy' },
//   { type: 'colon', value: ':' },
//   { type: 'valueNull', value: 'null' },
//   { type: 'objectEnd', value: '}' },
//   { type: 'objectEnd', value: '}' }
// ]

// // 解析结果 -------------------
// {
   
//   name: '周小黑',
//   age: 18,
//   marriage: true,
//   hobby: [ '吃烟', '喝酒', '烫头' ],
//   son: { nickname: '小馒头', toy: null }
// }

JSON.stringify
下面是一个简版的 JSON.stringify,只是为了展示核心原理,很多异常情况并未处理,主要就是利用递归方法去处理值里的对象和数组,其他的基本数据类型只用直接转成对应的 toString 形式拼接进去就行了:

function jsonStringify(obj) {
   
  function fmtValue(value) {
   
    if (value === null) {
   
      return 'null'
    } else if (typeof value === 'string') {
   
      return `"${
     value}"`
    } else if (typeof value === 'number') {
   
      return value.toString()
    } else if (typeof value === 'boolean') {
   
      return value.toString()
    } else if (typeof value === 'object') {
   
      if (Array.isArray(value)) {
   
        let res = '['
        for (var i = 0; i < value.length; i++) {
   
          res += (i ? ', ' : '') + fmtValue(value[i])
        }
        return res + ']'
      } else if (Object.prototype.toString.call(value) === '[object Object]') {
   
        let arr = []
        for (var k in value) {
   
          if (value.hasOwnProperty(k)) {
   
            const txt = `"${
     k}":` + fmtValue(value[k])
            arr.push(txt)
          }
        }
        return '{' + arr.join(', ') + '}'
      }
    }
  }

  function main(object) {
   
    let list = []
    const keys = Object.keys(object)
    keys.map(key => {
   
      let txt =  `"${
     key}":` + fmtValue(object[key])
      list.push(txt)
    })

    return '{' + list.join(',') + '}'
  }

  return main(obj)
}
相关文章
|
1月前
|
JSON 自然语言处理 前端开发
【面试题】JSON.stringify 和fast-json-stringify有什么区别
【面试题】JSON.stringify 和fast-json-stringify有什么区别
|
1月前
|
JSON 前端开发 Java
【面试题】对 JSON.stringify()与JSON.parse() 理解
【面试题】对 JSON.stringify()与JSON.parse() 理解
|
1月前
|
JSON API 数据格式
JSON.stringify()与JSON.parse()没有你想的那样简单
JSON.stringify()与JSON.parse()没有你想的那样简单
|
3月前
|
存储 JSON 缓存
json.stringify()的使用
json.stringify()的使用
|
存储 JSON JavaScript
JSON.stringify()和JSON.parse() 的使用总结
JSON.stringify()和JSON.parse() 的使用总结
|
存储 JSON JavaScript
JSON.stringify的使用
项目中遇到一个 bug,一个组件为了保留一份 JSON 对象,使用 JSON.stringify 将其转换成字符串,这样做当然是为了避免对象是引用类型造成数据源的污染。
|
JavaScript
js对象深拷贝JSON.stringify、JSON.parse
js对象深拷贝JSON.stringify、JSON.parse
|
存储 JSON 前端开发
深入理解JSON.stringify()
深入理解JSON.stringify()
深入理解JSON.stringify()
|
XML JSON 数据格式
Jayway - Json-Path 使用(一)
Jayway - Json-Path 使用(一)
532 0
Jayway - Json-Path 使用(一)