手写JSON.parse和JSON.stringify

搞懂了有限状态机,手写各种解析器都不在话下,主要区别也就是考虑怎么去设计各种状态流转。

手写JSON.parse

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

初级版本 JSON parse

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

javascript 复制代码
function parse(json) {
  const txt = '(' + json + ')'
  return eval(txt)
}

高级版本 JSON parse

主要利用有限状态机来做分词,然后再根据拿到的分词数据组装成 json 对象。

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

scss 复制代码
// 分词
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:

ini 复制代码
// 解析
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]
}

测试效果

perl 复制代码
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 形式拼接进去就行了:

javascript 复制代码
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)
}
相关推荐
前端大卫14 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘30 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare31 分钟前
浅浅看一下设计模式
前端
Lee川34 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端