今天,在學習 Node.js 中的 Buffer 對象時,注意到它的 alloc 和 from 方法會默認用 UTF-8 編碼,在數組中每位對應 1 字節的十六進制數。想到了之間學習 ES6 時關於字符串的 Unicode 表示法,忽然就很想知道 UTF-16 是如何進行編碼的,我嘗試將一些漢字轉換成二進制數,而後簡單的按 2 個字節一組轉換成十六進制,發現對於那些碼點較大的漢字,結果並不只僅是簡單的二進制轉十六進制。因而,我開始在網上找資料,決心完全弄明白 Unicode 編碼。javascript
在學校學 C 語言的時候,瞭解到一些計算機內部的機制,知道全部的信息最終都表示爲一個二進制的字符串,每個二進制位有 0 和 1 兩種狀態,經過不一樣的排列組合,使用 0 和 1 就能夠表示世界上全部的東西,感受有點中國「太極」的感受——「太極生兩儀,兩儀生四象,四象生八卦」。java
在計算機種中,1 字節對應 8 位二進制數,而每位二進制數有 0、1 兩種狀態,所以 1 字節能夠組合出 256 種狀態。若是這 256 中狀態每個都對應一個符號,就能經過 1 字節的數據表示 256 個字符。美國人因而就制定了一套編碼(其實就是個字典),描述英語中的字符和這 8 位二進制數的對應關係,這被稱爲 ASCII 碼。數組
ASCII 碼一共定義了 128 個字符,例如大寫的字母 A 是 65(這是十進制數,對應二進制是0100 0001)。這 128 個字符只使用了 8 位二進制數中的後面 7 位,最前面的一位統一規定爲 0。post
英語用 128 個字符來編碼徹底是足夠的,可是用來表示其餘語言,128 個字符是遠遠不夠的。因而,一些歐洲的國家就決定,將 ASCII 碼中閒置的最高位利用起來,這樣一來就能表示 256 個字符。可是,這裏又有了一個問題,那就是不一樣的國家的字符集可能不一樣,就算它們都能用 256 個字符表示全,可是同一個碼點(也就是 8 位二進制數)表示的字符可能可能不一樣。例如,144 在阿拉伯人的 ASCII 碼中是 گ,而在俄羅斯的 ASCII 碼中是 ђ。學習
所以,ASCII 碼的問題在於儘管全部人都在 0 - 127 號字符上達成了一致,但對於 128 - 255 號字符上卻有不少種不一樣的解釋。與此同時,亞洲語言有更多的字符須要被存儲,一個字節已經不夠用了。因而,人們開始使用兩個字節來存儲字符。ui
各類各樣的編碼方式成了系統開發者的噩夢,由於他們想把軟件賣到國外。因而,他們提出了一個「內碼錶」的概念,能夠切換到相應語言的一個內碼錶,這樣才能顯示相應語言的字母。在這種狀況下,若是使用多語種,那麼就須要頻繁的在內碼錶內進行切換。編碼
最終,美國人意識到他們應該提出一種標準方案來展現世界上全部語言中的全部字符,出於這個目的,Unicode誕生了。spa
Unicode 固然是一本很厚的字典,記錄着世界上全部字符對應的一個數字。具體是怎樣的對應關係,又或者說是如何進行劃分的,就不是咱們考慮的問題了,咱們只用知道 Unicode 給全部的字符指定了一個數字用來表示該字符。code
對於 Unicode 有一些誤解,它僅僅只是一個字符集,規定了符合對應的二進制代碼,至於這個二進制代碼如何存儲則沒有任何規定。它的想法很簡單,就是爲每一個字符規定一個用來表示該字符的數字,僅此而已。對象
以前提到,Unicode 沒有規定字符對應的二進制碼如何存儲。以漢字「漢」爲例,它的 Unicode 碼點是 0x6c49,對應的二進制數是 110110001001001,二進制數有 15 位,這也就說明了它至少須要 2 個字節來表示。能夠想象,在 Unicode 字典中日後的字符可能就須要 3 個字節或者 4 個字節,甚至更多字節來表示了。
這就致使了一些問題,計算機怎麼知道你這個 2 個字節表示的是一個字符,而不是分別表示兩個字符呢?這裏咱們可能會想到,那就取個最大的,假如 Unicode 中最大的字符用 4 字節就能夠表示了,那麼咱們就將全部的字符都用 4 個字節來表示,不夠的就往前面補 0。這樣確實能夠解決編碼問題,可是卻形成了空間的極大浪費,若是是一個英文文檔,那文件大小就大出了 3 倍,這顯然是沒法接受的。
因而,爲了較好的解決 Unicode 的編碼問題, UTF-8 和 UTF-16 兩種當前比較流行的編碼方式誕生了。固然還有一個 UTF-32 的編碼方式,也就是上述那種定長編碼,字符統一使用 4 個字節,雖然看似方便,可是卻不如另外兩種編碼方式使用普遍。
UTF-8 是一個很是驚豔的編碼方式,漂亮的實現了對 ASCII 碼的向後兼容,以保證 Unicode 能夠被大衆接受。
UTF-8 是目前互聯網上使用最普遍的一種 Unicode 編碼方式,它的最大特色就是可變長。它可使用 1 - 4 個字節表示一個字符,根據字符的不一樣變換長度。編碼規則以下:
對於單個字節的字符,第一位設爲 0,後面的 7 位對應這個字符的 Unicode 碼點。所以,對於英文中的 0 - 127 號字符,與 ASCII 碼徹底相同。這意味着 ASCII 碼那個年代的文檔用 UTF-8 編碼打開徹底沒有問題。
對於須要使用 N 個字節來表示的字符(N > 1),第一個字節的前 N 位都設爲 1,第 N + 1 位設爲0,剩餘的 N - 1 個字節的前兩位都設位 10,剩下的二進制位則使用這個字符的 Unicode 碼點來填充。
編碼規則以下:
Unicode 十六進制碼點範圍 | UTF-8 二進制 |
---|---|
0000 0000 - 0000 007F | 0xxxxxxx |
0000 0080 - 0000 07FF | 110xxxxx 10xxxxxx |
0000 0800 - 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000 - 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
根據上面編碼規則對照表,進行 UTF-8 編碼和解碼就簡單多了。下面以漢字「漢」爲利,具體說明如何進行 UTF-8 編碼和解碼。
「漢」的 Unicode 碼點是 0x6c49(110 1100 0100 1001),經過上面的對照表能夠發現,0x0000 6c49
位於第三行的範圍,那麼得出其格式爲 1110xxxx 10xxxxxx 10xxxxxx
。接着,從「漢」的二進制數最後一位開始,從後向前依次填充對應格式中的 x,多出的 x 用 0 補上。這樣,就獲得了「漢」的 UTF-8 編碼爲 11100110 10110001 10001001
,轉換成十六進制就是 0xE6 0xB7 0x89
。
解碼的過程也十分簡單:若是一個字節的第一位是 0 ,則說明這個字節對應一個字符;若是一個字節的第一位1,那麼連續有多少個 1,就表示該字符佔用多少個字節。
在瞭解 UTF-16 編碼方式以前,先了解一下另一個概念——"平面"。
在上面的介紹中,提到了 Unicode 是一本很厚的字典,她將全世界全部的字符定義在一個集合裏。這麼多的字符不是一次性定義的,而是分區定義。每一個區能夠存放 65536 個(2^16
)字符,稱爲一個平面(plane)。目前,一共有 17 個(2^5
)平面,也就是說,整個 Unicode 字符集的大小如今是 2^21
。
最前面的 65536 個字符位,稱爲基本平面(簡稱 BMP ),它的碼點範圍是從 0 到 2^16-1
,寫成 16 進制就是從 U+0000 到 U+FFFF。全部最多見的字符都放在這個平面,這是 Unicode 最早定義和公佈的一個平面。剩下的字符都放在輔助平面(簡稱 SMP ),碼點範圍從 U+010000 到 U+10FFFF。
基本瞭解了平面的概念後,再說回到 UTF-16。UTF-16 編碼介於 UTF-32 與 UTF-8 之間,同時結合了定長和變長兩種編碼方法的特色。它的編碼規則很簡單:基本平面的字符佔用 2 個字節,輔助平面的字符佔用 4 個字節。也就是說,UTF-16 的編碼長度要麼是 2 個字節(U+0000 到 U+FFFF),要麼是 4 個字節(U+010000 到 U+10FFFF)。那麼問題來了,當咱們遇到兩個字節時,究竟是把這兩個字節看成一個字符仍是與後面的兩個字節一塊兒看成一個字符呢?
這裏有一個很巧妙的地方,在基本平面內,從 U+D800 到 U+DFFF 是一個空段,即這些碼點不對應任何字符。所以,這個空段能夠用來映射輔助平面的字符。
輔助平面的字符位共有 2^20
個,所以表示這些字符至少須要 20 個二進制位。UTF-16 將這 20 個二進制位分紅兩半,前 10 位映射在 U+D800 到 U+DBFF,稱爲高位(H),後 10 位映射在 U+DC00 到 U+DFFF,稱爲低位(L)。這意味着,一個輔助平面的字符,被拆成兩個基本平面的字符表示。
所以,當咱們遇到兩個字節,發現它的碼點在 U+D800 到 U+DBFF 之間,就能夠判定,緊跟在後面的兩個字節的碼點,應該在 U+DC00 到 U+DFFF 之間,這四個字節必須放在一塊兒解讀。
接下來,以漢字"𠮷"爲例,說明 UTF-16 編碼方式是如何工做的。
漢字"𠮷"的 Unicode 碼點爲 0x20BB7
,該碼點顯然超出了基本平面的範圍(0x0000 - 0xFFFF),所以須要使用四個字節表示。首先用 0x20BB7 - 0x10000
計算出超出的部分,而後將其用 20 個二進制位表示(不足前面補 0 ),結果爲0001000010 1110110111
。接着,將前 10 位映射到 U+D800 到 U+DBFF 之間,後 10 位映射到 U+DC00 到 U+DFFF 便可。U+D800
對應的二進制數爲 1101100000000000
,直接填充後面的 10 個二進制位便可,獲得 1101100001000010
,轉成 16 進制數則爲 0xD842
。同理可得,低位爲 0xDFB7
。所以得出漢字"𠮷"的 UTF-16 編碼爲 0xD842 0xDFB7
。
Unicode3.0 中給出了輔助平面字符的轉換公式:
H = Math.floor((c-0x10000) / 0x400)+0xD800 L = (c - 0x10000) % 0x400 + 0xDC00
根據編碼公式,能夠很方便的計算出字符的 UTF-16 編碼。