请看一道面试题:javascript
'😂'.length // ?
复制代码
其结果不是 1,而是 2。😂😂😂html
为何会是这样?java
本文主要解决这个问题。git
首先咱们从 Unicode 提及。做为一个程序员,咱们都应该或多或少了解其相关知识。程序员
世界上有那么多语言系统,每门语言又有那多文字字符。github
为了在计算机上表示这些字符,一个自然的想法就是给每一个字符一个编号。把每个字符映射成一个整数,这些数字的学名叫码位(code point)。好比:面试
'a'.charCodeAt(0) // 97
'姚'.charCodeAt(0) // 23002
复制代码
究竟得多少个码位才够呢?刚开始 Unicode
设计人员以为 2^16 (65536)就该足够了,因而产生了 UCS-2
。(注:事实上 Unicode
和 UCS
在最开始时不是一家。)app
取 16 次方,即说明 2 个字节数据就能表示一个字符了。这种编码方式多简单,不论是从性能仍是从实现上来讲,都看起来是一个不错的选择。所以,不少语言都采用了16 位编码的字符串,包括我们的 JavaScript
。ide
虽然 6 万多个字符足以包括世界上绝大多数经常使用字符,但事实上仍是不够的,Unicode
不断扩展。截止 2019 年 3 月,已收入 150 个书写系统,共计字符 137928 的。性能
统一用两个字节来存储一个字符,这种方式再也不一直有效。所以出现了不一样的编码标准,好比 UTF-8
、UTF-16
和 UTF-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
)。
0xD83D
与 0xDE02
构成一个代理对,来表示码位 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+D83D
和 U+DE02
),即用了 2 个码元。上述 JS 规范中明确得指出:“每一个元素都被认为是一个单独的 UTF-16
码元”。所以😂符号为 2。
本文完。