【JS迷你书】String类型与UTF-16

5,154 阅读5分钟

请看一道面试题:

'😂'.length // ?

其结果不是 1,而是 2。😂😂😂

为什么会是这样?

本文主要解决这个问题。

首先我们从 Unicode 说起。作为一个程序员,我们都应该或多或少了解其相关知识。

世界上有那么多语言系统,每门语言又有那多文字字符。

为了在计算机上表示这些字符,一个天然的想法就是给每个字符一个编号。把每一个字符映射成一个整数,这些数字的学名叫码位(code point)。比如:

'a'.charCodeAt(0// 97
'姚'.charCodeAt(0// 23002

究竟得多少个码位才够呢?刚开始 Unicode 设计人员觉得 2^16 (65536)就该足够了,于是产生了 UCS-2。(注:事实上 UnicodeUCS 在最开始时不是一家。)

取 16 次方,即说明 2 个字节数据就能表示一个字符了。这种编码方式多简单,不管是从性能还是从实现上来说,都看起来是一个不错的选择。因此,很多语言都采用了16 位编码的字符串,包括咱们的 JavaScript

虽然 6 万多个字符足以包括世界上绝大多数常用字符,但事实上还是不够的,Unicode 不断扩展。截止 2019 年 3 月,已收入 150 个书写系统,共计字符 137928 的。

统一用两个字节来存储一个字符,这种方式不再一直有效。因此出现了不同的编码标准,比如 UTF-8UTF-16UTF-32

这里主要说说与 JS 相关的 UTF-16

UTF-16 是一种变长表示,它对来自常用字符 UCS-2 的码位,仍然用 2 个字节表示。而对来新增非常用的码位却用 4 个字节表示。二者能互相区分开来,这是 UTF-16 的精妙之处所在。

另外需要说明的是,最开始的 2^16 那些数据中并非都映射满了。从 U+D800(55296) 到 U+DFFF(57343)共 2048 个码位,是永久保留的,不映射到任何 Unicode 字符。它的存在为 UTF-16 提供了方便。

举例来说,字符😂的码位是 U+1F602(128514),大于 65535,因此是后添加的字符。

首先用它先减去 65536,得到 62978,对应的二进制是 1111011000000010

然后左补充 0 至 20 位:00001111011000000010

再从中间切断成上下两值:0000111101(61) 和 1000000010(514)。

添加 0xD800(55296)到上值,以形成高位:55296 + 61 = 55357(0xD83D)。

添加 0xDC00(56320)到下值,以形成低位:56320+ 514 = 56834(0xDE02)。

0xD83D0xDE02构成一个代理对,来表示码位 U+1F602

可以验证如下:

'😂'.charCodeAt(0// 55357
'😂'.charCodeAt(1// 56834
'\u{1F602}' // 😂
'\uD83D\uDE02' // 😂
'\u{1F602}'[0] === '\uD83D' // true

此时,想必你也明白了文章开头的问题了:'😂'.length 之所以为 2,是因为 JS 至今仍然使用 UCS-2 那种 16 进制读取方式。

最后,我们来看一下 JS 规范《Ecma-262 Edition 5.1》 对此的描述:

The String type is the set of all finite ordered sequences of zero or more 16-bit unsigned integer values (“elements”). The String type is generally used to represent textual data in a running ECMAScript program, in which case each element in the String is treated as a code unit value (see Clause 6). Each element is regarded as occupying a position within the sequence. These positions are indexed with nonnegative integers. The first element (if any) is at position 0, the next element (if any) at position 1, and so on. The length of a String is the number of elements (i.e., 16-bit values) within it. The empty String has length zero and therefore contains no elements.


When a String contains actual textual data, each element is considered to be a single UTF-16 code unit. Whether or not this is the actual storage format of a String, the characters within a String are numbered by their initial code unit element position as though they were represented using UTF-16. All operations on Strings (except as otherwise stated) treat them as sequences of undifferentiated 16-bit unsigned integers; they do not ensure the resulting String is in normalised form, nor do they ensure language-sensitive results.

翻译如下:

字符串类型是由 0 位或 16 位以上无符号整数值(元素)组成的所有有限有序序列的集合。字符串类型通常用于表示运行中的ECMAScript 程序中的文本数据,在这种情况下,字符串中的每个元素都被视为码元值(参见第6条)。这些位置用非负整数作索引。第一个元素(如果有)位于位置 0,下一个元素(如果有)位于位置 1,以此类推。字符串的长度是元素的数量(即,16位值)。空字符串的长度为零,因此不包含任何元素。


当字符串包含实际的文本数据时,每个元素都被认为是一个单独的 UTF-16 码元。无论这是否是字符串的实际存储格式,字符串中的字符都是通过其初始码元元素位置进行编号的,就像使用 UTF-16 表示一样。所有字符串上的操作(除非另有说明)都将它们视为无差异 16 位无符号整数的序列,它们不能确保得到的字符串是标准格式的,也不能确保得到对语言敏感的结果。

其中提到了码元(code unit),是指最存储的最小单位,这里即 2 个字节。

前文讨论过,大于 65535 的码位会生成一个代理对(比如😂的码位是 U+1F602,代理对是
U+D83DU+DE02),即用了 2 个码元。上述 JS 规范中明确得指出:“每个元素都被认为是一个单独的 UTF-16 码元”。因此😂符号为 2。

本文完。

《JavaScript 迷你书》传送门,全面夯实基础

掘金收藏