在 JavaScript 開發的過程當中,咱們常常須要使用到一些簡單對象,好比將簡單對象做爲配置傳給其餘類庫。javascript
可是,你知道對於 JavaScript 來講,將配置寫成 JSON 字符串的形式會比直接寫簡單對象更快嗎?前端
什麼是簡單對象?java
在 JavaScript 中,咱們能夠直接使用一對大括號來定義一個簡單對象,好比:webpack
const obj = {
foo: 'hello world',
bar: {
baz: ['1', 20, 0x012c, 4000n, 50_000],
},
};
複製代碼
這產生了一個簡單對象,並將其做爲常量 obj 的值。這個簡單對象包含了 foo
和 bar
兩個字段,其中 foo
是一個字符串,而 bar
是另外一個簡單對象,其中又包含了 baz
字段,它是一個數組,包含了字符串 '1'
、十進制表示的數字 20
、十六進制表示的數字 300
、BigInt
類型的數字 4000
和帶有下劃線分隔符的數字 50000
。git
也許你注意到了,這雖然是一個簡單對象,可是它並不簡單,光是數字,就有這麼多種不一樣的寫法。github
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 是 JavaScript Object Notation 的縮寫,其實是一個按照必定格式進行編碼的簡單 JavaScript 對象的字符串表示,它只有很是少的語法,語法結構很是簡單。而由於 JSON 字符串的語法簡單,因此它所包含的數據類型也很是的少,可是對於大部分場景來講,這足夠了。
JSON 憑藉其簡單的語法,在對其進行解釋的時候,能夠徹底按照從前到後的順序進行解釋,每個字符的語義都是根據上文而肯定的,不會由於後面跟的內容不一樣而表現出不一樣的含義,所以在進行語法解析的時候,只須要一個棧結構,而後從前到後順序解析便可,不會出現回溯的狀況。而且由於 JSON 中沒有函數、沒有對象解構、沒有變量,什麼都沒有,只有 {}
、[]
、""
、,
幾種肯定的符號,以及 object
、array
、string
、number
、boolean
、null
幾種簡單的數據類型,而且寫法也較爲簡單單一,因此在解析的時候能夠大大減小判斷的次數。
所以相比於 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
函數的方案,eval
與 JSON.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…