本章简要介绍了 Unicode 以及 JavaScript 如何处理 Unicode。
Unicode 由 Joe Becker(施乐)、Lee Collins(苹果)和 Mark Davis(苹果)于 1987 年创立。其理念是创建一个通用的字符集,因为当时有许多不兼容的纯文本编码标准:8 位 ASCII 的众多变体、五大码(繁体中文)、GB 2312(简体中文)等等。在 Unicode 出现之前,不存在多语言纯文本标准,但存在富文本系统(例如苹果的 WorldScript),允许您组合多种编码。
Unicode 的第一个草案于 1988 年发布。之后工作继续进行,工作组也随之扩大。Unicode 联盟 于 1991 年 1 月 3 日成立。
Unicode 联盟是一个非营利性组织,致力于开发、维护和推广软件国际化标准和数据,特别是 Unicode 标准 [...]
Unicode 1.0 标准的第一卷于 1991 年 10 月发布,第二卷于 1992 年 6 月发布。
字符的概念看似简单,但它有很多方面。这就是 Unicode 成为如此复杂的标准的原因。以下是一些重要的基本概念:
如果代码单元大于单个字节,则字节顺序很重要。BOM 是文本开头的一个伪字符(可能编码为多个代码单元),用于指示代码单元是大端序(最高有效字节优先)还是小端序(最低有效字节优先)。没有 BOM 的文本默认为大端序。BOM 还指示使用的编码;UTF-8、UTF-16 等的 BOM 不同。此外,如果 Web 浏览器没有关于文本编码的其他信息,它还可以作为 Unicode 的标记。但是,BOM 的使用频率不高,原因如下:
规范为每个 Unicode 字符分配了几个属性,其中一些属性列举如下:
名称。英文名称,由大写字母 A-Z、数字 0-9、连字符 (-) 和 <空格> 组成。两个例子
代码点的范围最初是 16 位。在 Unicode 2.0 版(1996 年 7 月)中,它被扩展了:现在它被划分为 17 个平面,编号从 0 到 16。每个平面包含 16 位(十六进制表示法:0x0000-0xFFFF)。因此,在下面的十六进制范围内,超过四个最低有效位的数字表示平面的编号。
平面 1-16 称为辅助平面或星体平面。
UTF-32(Unicode 转换格式 32)是一种具有 32 位代码单元的格式。任何代码点都可以由单个代码单元编码,这使其成为唯一的固定长度编码;对于其他编码,编码一个点所需的单元数会有所不同。
UTF-16 是一种具有 16 位代码单元的格式,需要一到两个单元来表示一个代码点。BMP 代码点可以用单个代码单元表示。较高的代码点是 20 位(16 乘以 16 位),在减去 0x10000(BMP 的范围)之后。这些位被编码为两个代码单元(所谓的代理对):
下表(改编自 Unicode 标准 6.2.0,表 3-5)直观地显示了位的分布方式
代码点 | UTF-16 代码单元 |
xxxxxxxxxxxxxxxx(16 位) | xxxxxxxxxxxxxxxx |
pppppxxxxxxyyyyyyyyyy(21 位 = 5+6+10 位) | 110110qqqqxxxxxx 110111yyyyyyyyyy(qqqq = ppppp - 1) |
为了启用这种编码方案,BMP 有一个空洞,其中未使用的代码点的范围是 0xD800-0xDFFF。因此,前导代理、后导代理和 BMP 代码点的范围是不相交的,这使得解码在面对错误时更加稳健。以下函数将代码点编码为 UTF-16(稍后我们将看到一个使用它的例子)
function
toUTF16
(
codePoint
)
{
var
TEN_BITS
=
parseInt
(
'1111111111'
,
2
);
function
u
(
codeUnit
)
{
return
'\\u'
+
codeUnit
.
toString
(
16
).
toUpperCase
();
}
if
(
codePoint
<=
0xFFFF
)
{
return
u
(
codePoint
);
}
codePoint
-=
0x10000
;
// Shift right to get to most significant 10 bits
var
leadingSurrogate
=
0xD800
|
(
codePoint
>>
10
);
// Mask to get least significant 10 bits
var
trailingSurrogate
=
0xDC00
|
(
codePoint
&
TEN_BITS
);
return
u
(
leadingSurrogate
)
+
u
(
trailingSurrogate
);
}
UCS-2 是一种已弃用的格式,它使用 16 位代码单元来表示(仅限!)BMP 的代码点。当 Unicode 代码点的范围扩展到 16 位以上时,UTF-16 取代了 UCS-2。
如果最高位不是 0,则零之前的 1 的数量表示序列中有多少个代码单元。初始代码单元之后的所有代码单元都具有位前缀 10。因此,初始代码单元和后续代码单元的范围是不相交的,这有助于从编码错误中恢复。
UTF-8 已成为最流行的 Unicode 格式。最初,它的流行是由于它与 ASCII 向后兼容。后来,由于它在操作系统、编程环境和应用程序中的广泛而一致的支持,它获得了更大的吸引力。
JavaScript 处理 Unicode 源代码的方式有两种:内部(解析期间)和外部(加载文件时)。
在内部,JavaScript 源代码被视为 UTF-16 代码单元序列。根据 ECMAScript 规范第 6 节
ECMAScript 源文本表示为 Unicode 字符编码(版本 3.0 或更高版本)中的字符序列。[...] 出于本规范的目的,ECMAScript 源文本假定为 16 位代码单元序列。[...] 如果实际源文本以 16 位代码单元以外的形式编码,则必须将其视为首先转换为 UTF-16。
在标识符、字符串字面量和正则表达式字面量中,任何代码单元也可以通过 Unicode 转义序列 \uHHHH
表示,其中 HHHH
是四个十六进制数字。例如:
> var f\u006F\u006F = 'abc'; > foo 'abc' > var λ = 123; > \u03BB 123
这意味着您可以在字面量和变量名中使用 Unicode 字符,而无需离开源代码中的 ASCII 范围。
在字符串字面量中,还可以使用另一种转义:十六进制转义序列,它使用两位十六进制数字表示 0x00-0xFF 范围内的代码单元。例如:
> '\xF6' === 'ö' true > '\xF6' === '\u00F6' true
虽然内部使用 UTF-16,但 JavaScript 源代码通常不以该格式存储。当 Web 浏览器通过 <script>
标签加载源文件时,它会按如下方式确定编码
否则,如果文件是通过 HTTP(S) 加载的,那么 Content-Type
标头可以通过 charset
参数指定编码。例如
Content-Type: application/javascript; charset=utf-8
<script>
标签具有 charset
属性,则使用该编码。即使 type
属性包含有效的媒体类型,该类型也不能具有参数 charset
(如前面提到的 Content-Type
标头中那样)。这确保了 charset
和 type
的值不会冲突。否则,将使用 <script>
标签所在的文档的编码。例如,这是一个 HTML5 文档的开头,其中 <meta>
标签声明该文档的编码为 UTF-8
<!doctype html>
<html>
<head>
<meta
charset=
"UTF-8"
>
...
强烈建议您始终指定编码。如果不这样做,则会使用特定于区域设置的 默认编码。换句话说,人们会在不同国家/地区看到不同的文件。只有最低的 7 位在不同语言环境中相对稳定。
我的建议可以总结如下
一些代码压缩工具可以 将包含 7 位以上 Unicode 代码点的源代码转换为“7 位干净”的源代码。它们通过用 Unicode 转义序列替换非 ASCII 字符来做到这一点。例如,以下调用 UglifyJS 会转换文件 test.js:
uglifyjs -b beautify=false,ascii-only=true test.js
文件 test.js 如下所示
var
σ
=
'Köln'
;
UglifyJS 的输出如下所示
var
\
u03c3
=
"K\xf6ln"
;
考虑以下负面示例。有一段时间,库 D3.js 以 UTF-8 发布。当从编码不是 UTF-8 的页面加载它时,这会导致 错误,因为代码包含如下语句
var
π
=
Math
.
PI
,
ε
=
1
e
-
6
;
标识符 π 和 ε 未正确解码,并且未被识别为有效的变量名。此外,一些包含 7 位以上代码点的字符串文字也没有被正确解码。作为解决方法,您可以通过向 <script>
标签添加适当的 charset
属性来加载代码
<script
charset=
"utf-8"
src=
"d3.js"
></script>
JavaScript 字符串是 UTF-16 代码单元序列。根据 ECMAScript 规范,第 8.4 节
当字符串包含实际文本数据时,每个元素都被视为单个 UTF-16 代码单元。
如前所述,您可以在 字符串文字中使用 Unicode 转义序列和十六进制转义序列。例如,您可以通过将 o 与分音符(代码点 0x0308)组合来生成字符 ö:
> console.log('o\u0308') ö
这适用于 JavaScript 命令行,例如 Web 浏览器控制台和 Node.js REPL。您还可以将这种字符串插入网页的 DOM 中。
网络上有 许多不错的 Unicode 符号表。看看 Tim Whitlock 的 “Emoji Unicode 表”,并对现代 Unicode 字体中有多少符号感到惊讶。表中的符号都不是图像;它们都是字体字形。假设您想通过 JavaScript 显示一个位于星形平面中的 Unicode 字符(显然,这样做存在风险:并非所有字体都支持所有此类字符)。例如,考虑一头牛,代码点 0x1F404:。
您可以复制该字符并将其直接粘贴到您的 Unicode 编码的 JavaScript 源代码中
JavaScript 引擎将解码源代码(通常为 UTF-8),并创建一个包含两个 UTF-16 代码单元的字符串。 或者,您可以自己计算两个代码单元并使用 Unicode 转义序列。有一些 Web 应用程序可以执行此计算,例如:
先前定义的函数 toUTF16
也执行此操作
> toUTF16(0x1F404) '\\uD83D\\uDC04'
UTF-16 代理对 (0xD83D, 0xDC04) 确实编码了牛
如果字符串 包含代理对(编码单个代码点的两个代码单元),则 length
属性不再计算字素。它计算代码单元:
这可以通过库来解决,例如 Mathias Bynens 的 Punycode.js,它与 Node.js 捆绑在一起
> var puny = require('punycode'); > puny.ucs2.decode(str).length 1
如果您想在 字符串中搜索或比较字符串,则需要进行规范化——例如,通过库 unorm(由 Bjarke Walling 开发)。
JavaScript 正则表达式(请参阅 第 19 章)中对 Unicode 的支持非常有限。例如,无法匹配 Unicode 类别,例如“大写字母”。
行终止符会影响 匹配。行终止符是以下四个字符之一,在下表中指定:
代码单元 | 名称 | 字符转义序列 |
\u000A | 换行符 |
|
\u000D | 回车符 |
|
\u2028 | 行分隔符 | |
\u2029 | 段落分隔符 |
以下正则表达式结构基于 Unicode
\s \S
(空白、非空白)具有基于 Unicode 的定义
> /^\s$/.test('\uFEFF') true
.
(点)匹配除行终止符以外的所有代码单元(不是代码点!)。请参阅下一节,了解如何匹配任何代码点。/m
:在多行模式下,断言 ^
匹配输入的开头和行终止符之后。断言 $
匹配行终止符之前和输入的结尾。在非多行模式下,它们分别仅匹配输入的开头或结尾。其他重要的字符类具有基于 ASCII 而不是 Unicode 的定义
\d \D
(数字、非数字):数字等效于 [0-9]
。\w \W
(单词字符、非单词字符):单词字符等效于 [A-Za-z0-9_]
。
\b \B
(在单词边界处、单词内部):单词是单词字符的序列 ([A-Za-z0-9_]
)。例如,在字符串 'über'
中,字符类转义序列 \b
将字符 b 视为单词的开头
> /\bb/.test('über') true
要匹配任何 代码单元,您可以使用 [\s\S]
;请参阅 原子:常规。
要匹配任何代码点,您需要使用:[20]
([\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])
前面的模式的工作原理如下
([BMP code point]|[leading surrogate][trailing surrogate])
由于所有这些范围都不相交,因此该模式将在格式良好的 UTF-16 字符串中正确匹配代码点。
一些库 有助于 在 JavaScript 中处理 Unicode:
XRegExp 是一个正则表达式库,它有一个 官方插件,用于通过以下三种结构之一匹配 Unicode 类别、脚本、块和属性
\p{...} \p{^...} \P{...}
例如,\p{Letter}
匹配各种字母表中的字母,而 \p{^Letter}
和 \P{Letter}
都匹配所有其他代码点。 第 30 章 简要概述了 XRegExp。
有关 Unicode 的更多信息,请参阅 以下内容:
有关 JavaScript 中 Unicode 支持的信息,请参阅
以下人员为本章做出了贡献:Mathias Bynens (@mathias)、Anne van Kesteren (@annevk) 和 Calvin Metcalf (@CWMma)。