实现 JSON.parse 方法

65 阅读4分钟

思路

字符串转 token 后解析成对应对象或者数组

string => tokens => object

代码实现

token.js

定义不同类型数据类型

// 引入 Token 类所需的 TokenType
const Type = {
  auto: 0, // auto 的时候, c 是 : , { } [] 其中之一, 要自己判断
  colon: 1, // :
  comma: 2, // ,
  braceLeft: 3, // {
  braceRight: 4, // }
  bracketLeft: 5, // [
  bracketRight: 6, // ]
  keyword: 7, // true false null
  number: 8, // 123
  string: 9, // "name"
};

class Token {
  constructor(token_type, value) {
    const d = {
      ":": Type.colon,
      ",": Type.comma,
      "{": Type.braceLeft,
      "}": Type.braceRight,
      "[": Type.bracketLeft,
      "]": Type.bracketRight,
    };

    if (token_type === Type.auto) {
      this.type = d[value];
    } else {
      this.type = token_type;
    }
    this.value = value;
  }

  toString() {
    return `type: ${this.type}, len: ${String(this.value).length}, value: "${
      this.value
    }"`;
  }
}

tokenList.js 将文本转 token

const tokenList = (code) => {
  let i = 0;
  const length = code.length;
  const tokens = [];

  let is_open = true;
  while (i < length) {
    const c = code[i];
    // console.log("ccccc", `(${c})`);
    i++;
    if (isSpaces(c)) {
      continue;
    } else if (isSpecialChar(c)) {
      const t = new Token(Type.auto, c);
      tokens.push(t);
    } else if (c === '"' && is_open) {
      // 吃字符串
      const [result, index] = stringEnd(code, i);

      if (index !== -1) {
        const t = new Token(Type.string, result);
        i = index;
        tokens.push(t);
        is_open = !is_open;
      }
    } else if (c === '"' && !is_open) {
      is_open = !is_open;
      continue;
    } else if (isDigit(c)) {
      // 吃数字
      const [value, offset] = numberEnd(code, i);
      const t = new Token(Type.number, value);
      i += offset;
      tokens.push(t);
    } else if (isKeywordChar(c)) {
      // true false null
      const kvs = {
        t: "true",
        f: "false",
        n: "null",
      };
      let value = kvs[c];
      const t = new Token(Type.keyword, value);

      tokens.push(t);
      i += kvs[c].length;
    } else {
      console.log("错误", c, code.slice(i, i + 10));
      return;
    }
  }

  return tokens;
};

tokenParse.js

/**
 * 从 Token 列表中解析出 JSON 对象或数组。
 *
 * @param {Token[]} ts - 要解析的 Token 列表。
 * @returns {object|array} - 解析结果,可能是 JSON 对象或数组。
 */
const parse = (ts) => {
  const t = ts[0];
  ts.shift();

  if (t.type === Type.braceLeft) {
    const obj = {};
    while (ts[0].type !== Type.braceRight) {
      const k = ts[0];

      // 确保 k.type 必须是 string
      // 确保 _colon 必须是 colon
      ts.shift();
      ts.shift();

      const v = parse(ts);
      obj[k.value] = v.value || v;

      // 吃一个 逗号
      const _comma = ts[0];
      if (_comma.type === Type.comma) {
        ts.shift();
      }
    }

    // 结束 删除末尾的 '}'
    ts.shift();
    return obj;
  } else if (t.type === Type.bracketLeft) {
    const l = [];
    while (ts[0].type !== Type.bracketRight) {
      const v = parse(ts);

      // 吃一个 逗号
      const _comma = ts[0];
      if (_comma.type === Type.comma) {
        ts.shift();
      }

      l.push(v.value);
    }

    // 删除末尾的 ']'
    ts.shift();
    return l;
  } else {
    // console.log('value: ', t);
    return t;
  }
};

tool.js

/**
 * 从字符串中提取数字的长度,从指定的偏移量开始。
 *
 * @param {string} s1 - 要解析的字符串。
 * @param {number} offset - 开始解析的位置。
 * @returns {number|undefined} - 返回数字的长度,或者如果解析错误则返回 undefined。
 */
const numberEnd = (s1, offset) => {
  // 定义包含数字字符的字符串
  const digits = "1234567890";
  // 从指定的偏移量开始逐字符检查
  for (let i = 0; i < s1.length - offset; i++) {
    const c = s1[offset + i];
    // 如果当前字符不是数字字符,则返回当前位置 i,即数字的长度
    if (!digits.includes(c)) {
      let value = parseInt(s1.slice(offset - 1, i + offset));
      return [value, i];
    }
  }
  // 如果无法解析数字,则输出错误消息并返回 undefined
  console.log("错误, 数字解析错误");
  return undefined;
};

/**
 * 从字符串中提取子字符串,直到遇到双引号结束,处理转义字符。
 *
 * @param {string} s1 - 要解析的字符串。
 * @param {number} offset - 开始解析的位置。
 * @returns {[string, number]|undefined} - 返回提取的子字符串和结束位置的数组,或者如果解析错误则返回 undefined。
 */
const stringEnd = (s1, offset) => {
  // 定义转义字符及其对应的映射关系
  const bs = {
    b: "\b",
    f: "\f",
    n: "\n",
    r: "\r",
    t: "\t",
    "/": "/",
    '"': '"',
    "\\": "\\",
  };

  // 初始化结果字符串和游标位置
  let res = "";
  let i = offset;

  // 从指定的偏移量开始逐字符检查
  while (i < s1.length) {
    const a = s1[i];

    // 如果遇到双引号,表示字符串结束,返回结果字符串和当前位置
    if (a === '"') {
      return [res, i];
    } else if (a === "\\") {
      // 如果遇到转义字符
      const b = s1[i + 1]; // 获取转义字符后的字符
      if (b in bs) {
        // 如果是合法的转义字符
        res += bs[b]; // 将转义字符替换为其映射的字符
        i += 2; // 跳过转义字符和其后面的字符
      } else {
        console.log("** 错误, 不合法的转义字符: " + a + b);
        const example = '\\b \\f \\n \\r \\t \\/ \\" \\\\';
        console.log("合法的转义字符是: " + example);
        return ["", -1]; // 返回空字符串和 -1 表示解析错误
      }
    } else {
      res += a; // 将当前字符添加到结果字符串中
      i += 1; // 移动游标位置到下一个字符
    }
  }

  // 如果字符串未能正确结束,则输出错误消息并返回 undefined
  console.log("错误, 字符串解析错误");
  return undefined;
};

/**
 * 检查给定的字符是否为空格字符(包括空格、制表符、换行符等)。
 *
 * @param {string} char - 要检查的字符。
 * @returns {boolean} - 如果字符是空格字符,则返回 true;否则返回 false。
 */
const isSpaces = (char) => {
  const spaces = " \b\f\n\r\t";
  return spaces.includes(char);
};

/**
 * 检查给定的字符是否是特定字符集合中的一个。
 *
 * @param {string} char - 要检查的字符。
 * @returns {boolean} - 如果字符是特定字符集合中的一个,则返回 true;否则返回 false。
 */
const isSpecialChar = (char) => {
  // 定义特定字符集合
  const specialChars = ":,[]{}";

  // 使用字符串的 includes 方法检查给定字符是否在特定字符集合中
  return specialChars.includes(char);
};

/**
 * 检查给定的字符是否是数字字符。
 *
 * @param {string} char - 要检查的字符。
 * @returns {boolean} - 如果字符是数字字符,则返回 true;否则返回 false。
 */
const isDigit = (char) => {
  // 定义包含数字字符的字符串
  const digits = "0123456789";

  // 使用字符串的 includes 方法检查给定字符是否在数字字符字符串中
  return digits.includes(char);
};

/**
 * 检查给定的字符是否是特定字符集合中的一个。
 *
 * @param {string} char - 要检查的字符。
 * @returns {boolean} - 如果字符是特定字符集合中的一个,则返回 true;否则返回 false。
 */
const isKeywordChar = (char) => {
  // 定义特定字符集合
  const keywordChars = "tfn";

  // 使用字符串的 includes 方法检查给定字符是否在特定字符集合中
  return keywordChars.includes(char);
};

main.js

let t0 = `
{
  "s1": "xigua",
  "s2": "a\bb\fc\nd\re\tf\\/hi",
  "num1": 11,
  "bool": true,
  "null": null,
  "arr1": [1, 2, 3],
  "obj": {
      "bool2": false,
      "arr2": [4, 5, 6],
      "num2": 123
  }
}
`;
const ts = tokenList(t0);
console.log("ts", ts);

const o = parse(ts);
console.log("o", o);

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>json parse</title>
  </head>
  <body>
    <script src="./token.js"></script>
    <script src="./tool.js"></script>
    <script src="./tokenList.js"></script>
    <script src="./jsonParse.js"></script>
    <script src="./main.js"></script>
  </body>
</html>

上面目录结构

├── index.html
├── main.js
├── token.js
├── tokenList.js
└── tokenParse.js 
└── tool.js