Javascript is required
原理解析发布于 2026-07-28更新于 2026-08-08审校于 2026-08-087 分钟阅读

JSON Unicode 转义:\uXXXX、代理对与 UTF-8

接口中看到 \u4e2d\u6587 并不代表内容被加密,它只是 JSON 字符串的一种 Unicode 转义写法。每个 \uXXXX 恰好描述一个 16 位 UTF-16 码元;BMP 字符通常用一个码元,Emoji 和部分生僻字需要一对代理项。本文区分码点、码元和 UTF-8 字节,并给出不拆坏代理对的转换与验证方法。

JSONUnicodeUTF-16UTF-8代理对

一、最小示例:转义形式与原生字符解析后相同

JSON 字符串可以直接包含 Unicode 字符,也可以用 \u 加四位十六进制数字表示码元。标准解析器会在 JSON.parse 时把转义序列还原到 JavaScript 字符串。

注意下面代码中的双反斜杠属于 JavaScript 源码字符串层;传给 JSON.parse 的实际文本包含单个反斜杠。这个层级区别可参考 /articles/nested-json-escaping

const escapedJson = '"\\u4e2d\\u6587"';
const nativeJson = '"中文"';

const a = JSON.parse(escapedJson);
const b = JSON.parse(nativeJson);

console.log(a); // 中文
console.log(a === b); // true

二、\uXXXX 表示 UTF-16 码元,不是 UTF-8 字节

JSON 的 \uXXXX 固定为四个十六进制数字,对应一个 16 位码元。JavaScript 字符串也以 UTF-16 码元序列为基础,因此 charCodeAt 返回的是码元值,而 codePointAt 可以在代理对起始位置返回完整码点。

UTF-8 是网络和文件常用的字节编码。\u4e2d 六个 ASCII 字符与汉字“中”的 UTF-8 三个字节是两种不同表示,不能把十六进制码元直接当作 UTF-8 字节。

const text = '中';

console.log(text.charCodeAt(0).toString(16)); // 4e2d
console.log(text.codePointAt(0)?.toString(16)); // 4e2d
console.log(new TextEncoder().encode(text));
// Uint8Array(3) [228, 184, 173]

三、辅助平面字符需要两个 \uXXXX 组成代理对

码点 U+0000 到 U+FFFF 位于基本多文种平面,但 U+D800 到 U+DFFF 保留为代理项。超过 U+FFFF 的字符会在 UTF-16 中使用高代理项和低代理项两个码元表示。

例如 🚀 的码点是 U+1F680,在 JSON 中可写成 \ud83d\ude80。JavaScript 的 length 统计码元,因此结果为 2;Array.from 或字符串迭代按码点处理,结果长度为 1。

const rocket = JSON.parse('"\\ud83d\\ude80"');

console.log(rocket); // 🚀
console.log(rocket.length); // 2 个 UTF-16 码元
console.log(Array.from(rocket).length); // 1 个码点
console.log(rocket.codePointAt(0)?.toString(16)); // 1f680

四、优先使用原生 Unicode,按需生成全转义 JSON

现代系统之间交换 JSON 时应使用 UTF-8。原生中文更便于阅读和调试;\uXXXX 通常只在 ASCII-only 输出、旧系统兼容或需要显式展示码元时使用。

JSON.stringify 会自动转义引号、反斜杠和控制字符,但不会默认把所有非 ASCII 字符都转成 \uXXXX。若协议确实要求 ASCII-only,可以在 stringify 结果上逐个处理 UTF-16 码元;这样 Emoji 会自然变成两个代理转义。

function stringifyAsciiOnly(value: unknown): string {
  return JSON.stringify(value).replace(/[\u007f-\uffff]/g, (unit) =>
    `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`,
  );
}

console.log(stringifyAsciiOnly({ text: '中文 🚀' }));
// {"text":"\u4e2d\u6587 \ud83d\ude80"}

五、解码时使用 JSON.parse,不要手写 \u 替换器

手写正则常忽略代理对、反斜杠层级、转义引号和非法输入。若输入是完整 JSON,直接 JSON.parse;若输入声称是一个 JSON 字符串字面量,应先验证解析结果确实为 string。

不要使用 eval 或 Function 解析不可信输入。它们接受的语法范围与 JSON 不同,并会引入代码执行风险。

function parseJsonStringLiteral(literal: string): string {
  const value: unknown = JSON.parse(literal);
  if (typeof value !== 'string') {
    throw new TypeError('Expected a JSON string literal');
  }
  return value;
}

console.log(parseJsonStringLiteral('"\\u4e2d\\u6587"'));
// 中文

六、原生字符与转义形式的体积不能只看字符数

以“中”为例,原生 UTF-8 是 3 个字节,而 ASCII 文本 \u4e2d 是 6 个字节;在未压缩情况下,转义形式更大。Emoji 的 UTF-8 通常为 4 个字节,而代理对转义包含 12 个 ASCII 字节。

实际 HTTP 传输还可能经过 Gzip 或 Brotli,压缩后差异取决于整份数据,不能从单个字符推导固定百分比。需要用 TextEncoder 测原始字节,并在相同压缩设置下比较完整响应。

const encoder = new TextEncoder();
const native = '中';
const escaped = '\\u4e2d';

console.log(encoder.encode(native).byteLength); // 3
console.log(encoder.encode(escaped).byteLength); // 6

七、孤立代理项是需要单独防范的异常边界

合法 JSON 语法可能包含单独的 \ud800,但它并不组成完整 Unicode 标量值。不同系统在编码、显示和长度处理上可能表现不同。字符串截断如果恰好切在代理对中间,也会制造孤立代理项。

现代 JavaScript 可用 isWellFormed 检查字符串是否包含孤立代理项,并用 toWellFormed 把它们替换为 U+FFFD。若目标环境较旧,应在进入 UTF-8 编码、URI 编码或跨系统传输前自行验证。

const broken = JSON.parse('"\\ud800"') as string;

if ('isWellFormed' in String.prototype) {
  console.log(broken.isWellFormed()); // false
  console.log(broken.toWellFormed()); // �
}

八、验证、FAQ 与结论

验证应覆盖 BMP 中文、Emoji、生僻字、组合字符、引号、反斜杠和孤立代理项。对每个样本执行 stringify → parse,断言字符串保持一致;若协议要求 ASCII-only,再断言输出只包含 ASCII,并确认重新 parse 后值不变。可使用 /tools/dev/unicode 查看码点与码元。

问:\uXXXX 是加密或压缩吗?答:都不是,它是 JSON 字符串的转义表示。

问:为什么一个 Emoji 会出现两个 \u?答:它的码点超过 U+FFFF,在 UTF-16 中由两个代理码元表示。

问:接口必须把中文全部转义吗?答:通常不需要;跨系统 JSON 应使用 UTF-8,是否转义不改变解析后的字符串。

结论:理解 \uXXXX 时必须区分码点、UTF-16 码元和 UTF-8 字节。解析用 JSON.parse,生成用 JSON.stringify;只有协议明确要求时才做全量 ASCII 转义。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具