包含 JSON,即 JSON ⊂ ECMAScript

发布日期 · 标签:ES2019

通过 JSON ⊂ ECMAScript 提案,JSON 成为 ECMAScript 的语法子集。如果你对这之前不是这样感到惊讶,你并不孤单!

旧的 ES2018 行为 #

在 ES2018 中,ECMAScript 字符串字面量不能包含未转义的 U+2028 LINE SEPARATOR 和 U+2029 PARAGRAPH SEPARATOR 字符,因为即使在该上下文中,它们也被视为行终止符。

// 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.node.org.cn/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 支持 #