請看一道面試題:
'?'.length // ?
複製程式碼
其結果不是 1,而是 2。???
為什麼會是這樣?
本文主要解決這個問題。
首先我們從 Unicode 說起。作為一個程式設計師,我們都應該或多或少了解其相關知識。
世界上有那麼多語言系統,每門語言又有那多文字字元。
為了在計算機上表示這些字元,一個天然的想法就是給每個字元一個編號。把每一個字元對映成一個整數,這些數字的學名叫碼位(code point)。比如:
'a'.charCodeAt(0) // 97
'姚'.charCodeAt(0) // 23002
複製程式碼
究竟得多少個碼位才夠呢?剛開始 Unicode
設計人員覺得 2^16 (65536)就該足夠了,於是產生了 UCS-2
。(注:事實上 Unicode
和 UCS
在最開始時不是一家。)
取 16 次方,即說明 2 個位元組資料就能表示一個字元了。這種編碼方式多簡單,不管是從效能還是從實現上來說,都看起來是一個不錯的選擇。因此,很多語言都採用了16 位編碼的字串,包括我們們的 JavaScript
。
雖然 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 的會生成一個代理對來表示,即用了 2 個碼元。上述 JS 規範中明確得指出:“每個元素都被認為是一個單獨的 UTF-16
碼元”。
本文完。