手写JSON.parse和JSON.stringify

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

手写JSON.parse

有两种实现方式,第1种初级版没啥难度,第2种利用状态机自己去解析字符流,需要先学习下编译原理相关的知识,否则理解起来可能有点蒙。

初级版本 JSON parse

直接通过 eval 函数实现,不过注意需要在 json 字符串前后拼上括号,否则会当成代码块报错解析导致报错:

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

be85c8e453b98af65d59cf790f22a903.jpeg

高级版本 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)
}
  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值