内置函数篇(下)

119 阅读5分钟

笔者最近在对原生JS的知识做系统梳理,因为我觉得JS作为前端工程师的根本技术,学再多遍都不为过。打算来做一个系列,以一系列的问题为驱动,当然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提升,高级选手也会得到复习和巩固。

第一章:实现 JSON.stringify

JSON.stringify([, replacer [, space]) 方法是将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串。此处模拟实现,不考虑可选的第二个参数 replacer 和第三个参数 space,如果对这两个参数的作用还不了解,建议阅读 MDN 文档。

  1. 基本数据类型:
    • undefined 转换之后仍是 undefined(类型也是 undefined)
    • boolean 值转换之后是字符串 "false"/"true"
    • number 类型(除了 NaN 和 Infinity)转换之后是字符串类型的数值
    • symbol 转换之后是 undefined
    • null 转换之后是字符串 "null"
    • string 转换之后仍是string
    • NaN 和 Infinity 转换之后是字符串 "null"
  2. 函数类型:转换之后是 undefined
  3. 如果是对象类型(非函数)
    • 如果是一个数组:如果属性值中出现了 undefined、任意的函数以及 symbol,转换成字符串 "null" ;
    • 如果是 RegExp 对象:返回 {} (类型是 string);
    • 如果是 Date 对象,返回 Date 的 toJSON 字符串值;
    • 如果是普通对象;
      • 如果有 toJSON() 方法,那么序列化 toJSON() 的返回值。
      • 如果属性值中出现了 undefined、任意的函数以及 symbol 值,忽略。
      • 所有以 symbol 为属性键的属性都会被完全忽略掉。
  4. 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
function jsonStringify(data) {
  let dataType = typeof data;

  if (dataType !== 'object') {
    let result = data;
    //data 可能是 string/number/null/undefined/boolean
    if (Number.isNaN(data) || data === Infinity) {
      //NaN 和 Infinity 序列化返回 "null"
      result = "null";
    } else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') {
      //function 、undefined 、symbol 序列化返回 undefined
      return undefined;
    } else if (dataType === 'string') {
      result = '"' + data + '"';
    }
    //boolean 返回 String()
    return String(result);
  } else if (dataType === 'object') {
    if (data === null) {
      return "null"
    } else if (data.toJSON && typeof data.toJSON === 'function') {
      return jsonStringify(data.toJSON());
    } else if (data instanceof Array) {
      let result = [];
      //如果是数组
      //toJSON 方法可以存在于原型链中
      data.forEach((item, index) => {
        if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
          result[index] = "null";
        } else {
          result[index] = jsonStringify(item);
        }
      });
      result = "[" + result + "]";
      return result.replace(/'/g, '"');

    } else {
      //普通对象
      /**
       * 循环引用抛错(暂未检测,循环引用时,堆栈溢出)
       * symbol key 忽略
       * undefined、函数、symbol 为属性值,被忽略
       */
      let result = [];
      Object.keys(data).forEach((item, index) => {
        if (typeof item !== 'symbol') {
          //key 如果是symbol对象,忽略
          if (data[item] !== undefined && typeof data[item] !== 'function'
            && typeof data[item] !== 'symbol') {
            //键值如果是 undefined、函数、symbol 为属性值,忽略
            result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
          }
        }
      });
      return ("{" + result + "}").replace(/'/g, '"');
    }
  }
}

第二章:实现 JSON.parse

介绍模仿实现 JSON.parse 方法实现:

参考:JSON.parse 三种实现方式

用eval 实现

:::tips eval() 可以接受一个字符串Str作为参数,并把这个参数作为脚本代码执行 ::: 第一种方式最简单,也最直观,就是直接调用 eval,代码如下:

var json = '{"a":"1", "b":2}';
var obj = eval("(" + json + ")");  // obj 就是 json 反序列化之后得到的对象

但是直接调用 eval 会存在安全问题,如果数据中可能不是 json 数据,而是可执行的 JavaScript 代码,那很可能会造成 XSS 攻击。因此,在调用 eval 之前,需要对数据进行校验。

var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;

if (
  rx_one.test(
    json.replace(rx_two, "@")
      .replace(rx_three, "]")
      .replace(rx_four, "")
  )
) {
  var obj = eval("(" + json + ")");
}

用new Function 实现

Function 与 eval 有相同的字符串参数特性

var json = '{"name":"一缕清风", "age":20}'

// var obj = (new Function("params1", "params2", 'return ' + json))()
// Function的如果没有参数可以省略
var obj = (new Function('return ' + json))()
console.log(obj)                     // { name: '一缕清风', age: 20 }
console.log(obj.name)                // 一缕清风
console.log(toString.call(obj))      // [object Object]

用递归实现


// 调用核心的 next 函数,逐个读取字符
var next = function (c) {
  // If a c parameter is provided, verify that it matches the current character.
  if (c && c !== ch) {
    error("Expected '" + c + "' instead of '" + ch + "'");
  }

  // Get the next character. When there are no more characters,
  // return the empty string.
  ch = text.charAt(at);
  at += 1;
  return ch;
};

var value = function () {
  // Parse a JSON value. It could be an object, an array, a string, a number,
  // or a word.

  white();
  // 根据当前字符是什么,我们便能推导出后面应该接的是什么类型
  switch (ch) {
    case "{":
      return object();
    case "[":
      return array();
    case "\"":
      return string();
    case "-":
      return number();
    default:
      return (ch >= "0" && ch <= "9")
        ? number()
        : word();
  }
};

用状态机模式实现

状态机名字起得很抽象,应用也非常广泛,比如正则引擎、词法分析,甚至是字符串匹配的 KMP 算法都能用它来解释。它代表着一种本质的逻辑:在 A 状态下,如果输入 B,就会转移到 C 状态。 那么,状态机与 JSON 字符串的解析有什么关系呢?→ JSON 字符串是有格式规范的,比如 key 和 value 之间用冒号隔开,比如不同 key-value 对之间用逗号隔开……这些格式规范可以翻译成状态机的状态转移,比如“如果检测到冒号,那么意味着下一步可以输入 value” 等等。还是以'{"a":"1", "b":2}'为例,我们来看看对这个 JSON 字符串进行解析时,状态机都流经了哪些状态。 image.png

var string = {   // The actions for string tokens
  go: function () {
    state = "ok";
  },
  firstokey: function () {
    key = value;
    state = "colon";
  },
  okey: function () {
    key = value;
    state = "colon";
  },
  ovalue: function () {
    state = "ocomma";
  },
  firstavalue: function () {
    state = "acomma";
  },
  avalue: function () {
    state = "acomma";
  }
};

参考文献

:::info 内置函数篇(下) :::