今天看啥  ›  专栏  ›  ihap技术黑洞

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

ihap技术黑洞  · 掘金  ·  · 2019-12-30 07:48
阅读 371

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

在 JavaScript 开发的过程中,我们经常需要使用到一些简单对象,比如将简单对象作为配置传给其他类库。

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

简单对象

什么是简单对象?

在 JavaScript 中,我们可以直接使用一对大括号来定义一个简单对象,比如:

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

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

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

JavaScript 中的简单对象

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

比如,现在有这样的代码:

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…




原文地址:访问原文地址
快照地址: 访问文章快照