透過 JSON ⊂ ECMAScript 提案,JSON 成為 ECMAScript 的語法子集。如果您驚訝這不是既有情況,您並非孤單!
舊的 ES2018 行為 #
在 ES2018 中,ECMAScript 字串文字無法包含未跳脫的 U+2028 行分隔符號和 U+2029 段落分隔符號字元,因為它們即使在該語境中仍被視為換行符號
// A string containing a raw U+2028 character.
const LS = '
';
// → ES2018: SyntaxError
// A string containing a raw U+2029 character, produced by `eval`:
const PS = eval('"\u2029"');
// → ES2018: SyntaxError
這是有問題的,因為 JSON 字串可以包含這些字元。因此,開發人員在將有效的 JSON 嵌入 ECMAScript 程式中時,必須實作特殊的後處理邏輯來處理這些字元。沒有這種邏輯,程式碼將會有細微的錯誤,甚至 安全性問題!
新的行為 #
在 ES2019 中,字串文字現在可以包含原始的 U+2028 和 U+2029 字元,消除了 ECMAScript 和 JSON 之間令人困惑的不匹配。
// A string containing a raw U+2028 character.
const LS = '
';
// → ES2018: SyntaxError
// → ES2019: no exception
// A string containing a raw U+2029 character, produced by `eval`:
const PS = eval('"\u2029"');
// → ES2018: SyntaxError
// → ES2019: no exception
這個小小的改進大幅簡化了開發人員的心智模式(少記一個邊界情況!),並減少了在將有效的 JSON 嵌入 ECMAScript 程式中時,對特殊後處理邏輯的需求。
將 JSON 嵌入 JavaScript 程式中 #
由於這個提案,JSON.stringify
現在可用於產生有效的 ECMAScript 字串文字、物件文字和陣列文字。而且由於有單獨的 格式良好的 JSON.stringify
提案,這些文字可以安全地以 UTF-8 和其他編碼表示(如果您嘗試將它們寫入磁碟上的檔案,這會很有用)。這對於元程式設計使用案例非常有用,例如動態建立 JavaScript 原始碼並將其寫入磁碟。
以下是一個建立有效 JavaScript 程式碼的範例,其中嵌入給定的資料物件,利用 JSON 語法現在是 ECMAScript 的子集
// A JavaScript object (or array, or string) representing some data.
const data = {
LineTerminators: '\n\r
',
// Note: the string contains 4 characters: '\n\r\u2028\u2029'.
};
// Turn the data into its JSON-stringified form. Thanks to JSON ⊂
// ECMAScript, the output of `JSON.stringify` is guaranteed to be
// a syntactically valid ECMAScript literal:
const jsObjectLiteral = JSON.stringify(data);
// Create a valid ECMAScript program that embeds the data as an object
// literal.
const program = `const data = ${ jsObjectLiteral };`;
// → 'const data = {"LineTerminators":"…"};'
// (Additional escaping is needed if the target is an inline <script>.)
// Write a file containing the ECMAScript program to disk.
saveToDisk(filePath, program);
上述指令碼產生以下程式碼,評估後會得到一個等效的物件
const data = {"LineTerminators":"\n\r
"};
使用 JSON.parse
將 JSON 嵌入 JavaScript 程式中 #
如 JSON 的成本 中所述,您可以將資料內嵌為 JavaScript 物件文字,如下所示,而不是這樣
const data = { foo: 42, bar: 1337 }; // 🐌
…資料可以表示成 JSON 字串化形式,然後在執行階段進行 JSON 解析,以提升處理大型物件 (10 kB+) 的效能
const data = JSON.parse('{"foo":42,"bar":1337}'); // 🚀
以下是一個實作範例
// A JavaScript object (or array, or string) representing some data.
const data = {
LineTerminators: '\n\r
',
// Note: the string contains 4 characters: '\n\r\u2028\u2029'.
};
// Turn the data into its JSON-stringified form.
const json = JSON.stringify(data);
// Now, we want to insert the JSON into a script body as a JavaScript
// string literal per https://v8.dev.org.tw/blog/cost-of-javascript-2019#json,
// escaping special characters like `"` in the data.
// Thanks to JSON ⊂ ECMAScript, the output of `JSON.stringify` is
// guaranteed to be a syntactically valid ECMAScript literal:
const jsStringLiteral = JSON.stringify(json);
// Create a valid ECMAScript program that embeds the JavaScript string
// literal representing the JSON data within a `JSON.parse` call.
const program = `const data = JSON.parse(${ jsStringLiteral });`;
// → 'const data = JSON.parse("…");'
// (Additional escaping is needed if the target is an inline <script>.)
// Write a file containing the ECMAScript program to disk.
saveToDisk(filePath, program);
上述指令碼產生以下程式碼,評估後會得到一個等效的物件
const data = JSON.parse("{\"LineTerminators\":\"\\n\\r
\"}");
Google 的基準測試比較 JSON.parse
與 JavaScript 物件字面值在其建置步驟中利用此技術。Chrome DevTools 的「複製為 JS」功能已透過採用類似技術大幅簡化。
關於安全性的一則說明 #
JSON ⊂ ECMAScript 減少 JSON 與 ECMAScript 在字串字面值上的不匹配。由於字串字面值會出現在其他 JSON 支援的資料結構中,例如物件和陣列,因此它也處理那些案例,如上述程式碼範例所示。
不過,U+2028 和 U+2029 仍被 ECMAScript 語法的其他部分視為換行字元。這表示仍有案例無法安全地將 JSON 注入 JavaScript 程式中。考慮以下範例,其中伺服器在透過 JSON.stringify()
執行使用者提供的內容後,將其注入 HTML 回應中
<script>
// Debug info:
// User-Agent: <%= JSON.stringify(ua) %>
</script>
請注意,JSON.stringify
的結果會注入到指令碼中的單行註解中。
當如上述範例中所述使用時,JSON.stringify()
保證會傳回單行。問題在於「單行」的定義在 JSON 和 ECMAScript 之間不同。如果 ua
包含未跳脫的 U+2028 或 U+2029 字元,我們會跳出單行註解,並將 ua
的其餘部分當成 JavaScript 原始碼執行
<script>
// Debug info:
// User-Agent: "User-supplied string<U+2028> alert('XSS');//"
</script>
<!-- …is equivalent to: -->
<script>
// Debug info:
// User-Agent: "User-supplied string
alert('XSS');//"
</script>
注意:在上述範例中,原始未跳脫的 U+2028 字元表示為 <U+2028>
,以方便理解。
JSON ⊂ ECMAScript 在此無濟於事,因為它只影響字串字面值,而在這個案例中,JSON.stringify
的輸出會注入到不會直接產生 JavaScript 字串字面值的位置。
除非為這兩個字元引入特殊的後處理,否則上述程式碼片段會呈現跨網站指令碼漏洞 (XSS)!
注意:後處理使用者控制的輸入以跳脫任何特殊字元序列至關重要,這取決於脈絡。在此特定案例中,我們會注入到 <script>
標籤,因此我們必須 (也) 跳脫 </script
、<script
和 <!--
。
JSON ⊂ ECMAScript 支援 #
- Chrome: 自版本 66 起支援
- Firefox: 支援
- Safari: 支援
- Node.js: 自版本 10 起支援
- Babel: 支援