JS 中为啥使用 JSON 来代替简单对象会更快?

在 JavaScript 开发的过程当中,咱们常常须要使用到一些简单对象,好比将简单对象做为配置传给其余类库。javascript

可是,你知道对于 JavaScript 来讲,将配置写成 JSON 字符串的形式会比直接写简单对象更快吗?前端

简单对象

什么是简单对象?java

在 JavaScript 中,咱们能够直接使用一对大括号来定义一个简单对象,好比:webpack

const obj = {
  foo: 'hello world',
  bar: {
    baz: ['1', 20, 0x012c, 4000n, 50_000],
  },
};
复制代码

这产生了一个简单对象,并将其做为常量 obj 的值。这个简单对象包含了 foobar 两个字段,其中 foo 是一个字符串,而 bar 是另外一个简单对象,其中又包含了 baz 字段,它是一个数组,包含了字符串 '1'、十进制表示的数字 20、十六进制表示的数字 300BigInt 类型的数字 4000 和带有下划线分隔符的数字 50000git

也许你注意到了,这虽然是一个简单对象,可是它并不简单,光是数字,就有这么多种不一样的写法。github

JavaScript 中的简单对象

JavaScript 做为解释执行的语言,代码以纯文本的形式下载到浏览器(Node.JS 则不须要这一步),而后经过解释器进行解释、转为机器指令执行。在对代码进行解释的过程当中,因为须要考虑到代码的各类语法,而须要大量的时间进行判断,而且由于代码一般是从开始到结尾,一个字符一个字符的去解释的,因此甚至不少时候还须要回溯。web

好比,如今有这样的代码:数组

const value = 'any value';
const obj = ({ value▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
复制代码

当 JS 解释器解释到这里的时候,obj 后面的 value 表明了什么呢?还不肯定,取决于后面的代码,若是后面是长这个样子:浏览器

const value = 'any value';
const obj = ({ value: 1▓▓▓▓▓▓▓▓
复制代码

那么 obj 后面的 value 就与上面的 value 常量无关了。框架

可是,若是后面是长这个样子:

const value = 'any value';
const obj = ({ value })▓▓▓▓▓▓▓
复制代码

这时 obj 后面的 value 的值就是上面的 value 常量了……吗?并不必定,仍是要取决于后面更多的代码。若是后面跟着一个分号:

const value = 'any value';
const obj = ({ value });
复制代码

那么基本能够肯定了,obj 后面的 value 的值就是上面的 value 常量,所获得的结果就是 obj 是一个简单对象 { value: 'any value' }

可是,若是后面的代码是这个样子:

const value = 'any value';
const obj = ({ value }).value;
复制代码

那么此时 obj 后面的 value 的值也是上面的 value 常量,可是所获得的结果,obj 倒是字符串 'any value'

或者,若是后面的代码长这个样子:

const value = 'any value';
const obj = ({ value }) => value;
复制代码

那么此时代码的含义就彻底不一样了,obj 变成了一个箭头函数,后面的 value 做为解构的形参,再后面的 value 表明箭头函数的返回值。

因而可知,若是在代码中使用简单对象的话,JavaScript 解释器在进行语法解释的时候,须要进行大量的判断,甚至须要根据后面的代码内容来对前面的代码内容含义进行不同的解释。

而 JSON 字符串就不同了。

利用 JSON 来生成简单对象

那么,什么是 JSON?

JSON 是 JavaScript Object Notation 的缩写,其实是一个按照必定格式进行编码的简单 JavaScript 对象的字符串表示,它只有很是少的语法,语法结构很是简单。而由于 JSON 字符串的语法简单,因此它所包含的数据类型也很是的少,可是对于大部分场景来讲,这足够了。

JSON 凭借其简单的语法,在对其进行解释的时候,能够彻底按照从前到后的顺序进行解释,每个字符的语义都是根据上文而肯定的,不会由于后面跟的内容不一样而表现出不一样的含义,所以在进行语法解析的时候,只须要一个栈结构,而后从前到后顺序解析便可,不会出现回溯的状况。而且由于 JSON 中没有函数、没有对象解构、没有变量,什么都没有,只有 {}[]"", 几种肯定的符号,以及 objectarraystringnumberbooleannull 几种简单的数据类型,而且写法也较为简单单一,因此在解析的时候能够大大减小判断的次数。

所以相比于 JavaScript 代码的解释来讲,JSON 的解释就简单高效不少。

在 JavaScript 中,预先提供了 JSON.parse 函数,它接受一个 JSON 字符串做为参数,并返回这个 JSON 字符串解释获得的结果。

因为 JSON 的解释速度比 JavaScript 代码快不少,而且 JSON.parse 做为内置函数,JSON 字符串会交由 JavaScript 解释器内部的 C/C++ 代码进行解释,并直接跳过 JavaScript 语法解释器部分,直接在解释器内部生成并返回 JavaScript 对象的引用。

在这个过程当中,JavaScript 解释器只须要解释到 JSON.parse(',便可得知后面是一个字符串,并一路向后寻找字符串的结尾,这之间不会有复杂的语法(反引号 ``` 字符串中包含的 ${} 除外),最多就只有转义字符 \ 的转义。随后就会将解释所获得的字符串交给 JSON.parse 函数进行处理了。

所以,这个过程会比直接去解释一个 JavaScript 对象快得多(固然,具体快多少就要看不一样的 JavaScript 解释器的优化程度了)。

解析速度对比

固然,上面所说的这些到目前为止都仅仅局限于“理论”,而实际状况可能会与理论有必定差距,为了科学的严谨性(???),咱们须要作一些对比实验来验证这个结论。

首先,测试平台是 Windows 10 1909 Sandbox,i7-9700K,Google Chrome 79.0.3945.88 (64 位)、Mozilla Firefox 71.0(64 位)、Node.JS 13.5.0(64 位)。

⚠ 注意:下面的测试代码中,使用了一个 load.js 来动态引用加载两个待测试的 JS 文件。不能将 console.time 这类计时代码与待测试的代码放到一块儿,由于部分 JS 引擎(Firefox 与 Node.JS)会预先处理一遍 JS 代码,而后才开始执行,这样获得的结果就不许确了。

测试代码:

// JS.js
const use = () => {};
let times = 10_0000;
while (times--) {
  use({string:'string',number:1,array:['string',1],object:{Key1:'a',Key2:'b'}});
}

// JSON.js
const use = () => {};
let times = 10_0000;
while (times--) {
  use(JSON.parse('{"string":"string","number":1,"array":["string",1],"object":{"Key1":"a","Key2":"b"}}'));
}

// load.js
(async () => {
  console.time('JS');
  await import('./JS.mjs');
  console.timeEnd('JS');
  console.time('JSON');
  await import('./JSON.mjs');
  console.timeEnd('JSON');
})();
复制代码

嗯,看起来没有什么问题,咱们来试一下(下面的数据都是测试 10 次后取的平均值):

Chrome Firefox Node.JS
JS 19.6355 ms 413.2 ms 12.0093 ms
JSON 143.4788 ms 662 ms 105.9769 ms

emmmmmm……

画风怎么不太对?理论上 JSON 不是应该比 JS 快吗?怎么实际结果倒是 JSON 比 JS 慢这么多?

实际上,这里的测试方法是存在问题的!!!

再回顾一下上文中说的内容,JSON 比 JS 快的根本缘由是由于 JSON 的解析速度比 JS 更快!

那么何时会对代码进行解析呢?

实际上,上面的这段代码,JS 版本只会在第一次循环的时候对中间的简单对象进行解释,解释以后就会生成机器代码了,后续都不须要再进行解释。

而 JSON 版本则不一样,对于解释器来讲,每一次循环获得的都是一个字符串,所以这个字符串只须要解释一遍,可是因为涉及到一个 JSON.parse 函数调用,JSON.parse 函数接受一个字符串,而且在每一次调用时都会从新解析这一个字符串,即使是同一个字符串,JSON.parse 也会重复解析。

所以,这里的 JSON 版本其实是强制让 JavaScript 引擎对代码重复解释了十万次,而 JS 版本则只解释了一次。因此获得的结果就会是 JSON 比 JS 慢不少倍!

真·解析速度对比

那么,为了验证上文中 JSON 比 JS 更快这个结论,咱们应该怎样测试呢?

其实很简单,只须要确保 JSON 与 JS 的解释次数同样便可,或是它们都只解释一遍便可。

第一种方案

为了使 JSON 与 JS 的解释次数同样,咱们能够采用 eval 函数的方案,evalJSON.parse 同样是 JavaScript 的内置函数,不一样的是,eval 中是能够编写完整的 JavaScript 代码的,因此 eval 采用的依旧是 JS 解释器,而不是 JSON.parse 那样的 JSON 解释器。

测试代码:

// JS.js
const use = () => {};
let times = 10_0000;
while (times--) {
  use(eval(`({string:'string',number:1,array:['string',1],object:{Key1:'a',Key2:'b'}})`));
}

// JSON.js
const use = () => {};
let times = 10_0000;
while (times--) {
  use(eval(`JSON.parse('{"string":"string","number":1,"array":["string",1],"object":{"Key1":"a","Key2":"b"}}')`));
}

// load.js
(async () => {
  console.time('JS');
  await import('./JS.mjs');
  console.timeEnd('JS');
  console.time('JSON');
  await import('./JSON.mjs');
  console.timeEnd('JSON');
})();
复制代码

⚠ 注意:这里使用 eval 的代码是很是糟糕的,JavaScript 不得不建立十万个对象,而后进行垃圾回收,垃圾回收的时间会使得浏览器卡住较长时间,最终打印的时间却远没有等待的那么长。

嗯,看起来没有什么问题,咱们来试一下(下面的数据都是测试 10 次后取的平均值):

Chrome Firefox Node.JS
JS 4237.9201 ms 5815 ms 113.056 ms
JSON 4703.9819 ms 16244 ms 135.533 ms

emmmmmmmm......

二者的耗时都增长了,可是总体看来,JSON 仍是要比 JS 慢的,这是为何呢???

实际上,这是因为这里生成的简单对象实在是太“简单”了,它过短了,以致于二者并不能拉开差距。

可是就算拉不开差距,JSON 也不该该比 JS 更慢吧?

实际上,这种方案虽然强行让二者都解析了相同的次数,可是对 JSON 却并不公平,由于 JSON 版本在进行 JSON 解析以前,eval 还须要对 JSON.parse 这个函数调用自己进行解析,而且函数调用也存在上下文切换的时间开销。

所以,在 JSON 的长度比较短的情形下,JSON 解析速度的提高并不能弥补 JSON.parse 函数调用带来的开销,所以表现出来的结果就是,JSON 版本要比 JS 版本更慢。

因此,为了测量 JSON 与原生 JS 的真实解析速度,咱们仍是要使用第二种方案:

第二种方案

为了使 JSON 与 JS 都仅解释一遍,咱们的代码就只能是一层简单对象的建立。

而因为如今的电脑速度太快,因此只有在生成足够大的文件时,才能拉开差距,看到明显的时间差别。

为了获得一个足够大的文件,我找来了一个 1.8 MB 大小的 JSON 文件(国内省市区行政区划),分别将其直接做为 JS 简单对象和传给 JSON.parse 函数,进行时间比较:

测试代码(代码中 JSON 部分已省略):

// JS.js
const use = () => {};
console.time('JS');
use({...});
console.timeEnd('JS');

// JSON.js
const use = () => {};
console.time('JSON');
use(JSON.parse('{...}'));
console.timeEnd('JSON');


// JS.js
const use = () => {};
use({...});

// JSON.js
const use = () => {};
let = times = 10_0000;
use(JSON.parse('{...}'));

// load.js
(async () => {
  console.time('JS');
  await import('./JS.mjs');
  console.timeEnd('JS');
  console.time('JSON');
  await import('./JSON.mjs');
  console.timeEnd('JSON');
})();
复制代码

测试结果(下面的数据都是测试 10 次后取的平均值):

Chrome Firefox Node.JS
JS 157.7056 ms 122.5 ms 108.6904 ms
JSON 87.2692 ms 77.4 ms 57.665 ms

那么,既然 JSON.parse('{...}');{...}; 快,那么在平常开发中是否有必要将代码写成 JSON 的形式呢?

不用说,也应该知道,不该该!

若是将代码写成 JSON 字符串的形式,不只不易读,而且部分 JavaScript 的代码也没有办法直接使用 JSON 进行表示(好比 JavaScript 基本数据类型中的 undefined、Symbol、BigInt 都不能准确表示为 JSON),在绝大部分状况下,带来的速度提高也并不明显(0.01s 比 0.07s 快不了多少),所以带来的好处并无多少。

可是可是,若是是对于大型前端渲染类型的项目来讲,大部分 Web 框架都会有一大堆的配置代码,这些配置代码一般都是 string、number、boolean 这些基本类型,而且很是庞大,甚至能有几 MB 甚至是几十 MB。对于这些大型代码来讲,JSON 字符串的优化就能较为明显的体现出来了。

可是,可是,一般这些大型项目都会使用相似于 Webpack、Rollup 之类的工具进行项目处理,对于这样的优化操做,交由这些打包工具来进行就行了,平常写代码,该咋写,还咋写~

PS: Webpack 已在今年 7 月 2 日的提交中包含了这个优化操做,详情:github.com/webpack/web…

相关文章
相关标签/搜索