Unicode與JavaScript詳解

本文大部份內容轉自 阮一峯前輩的文章,更新了部份內容並加入了部分本身的理解。html

Unicode是什麼?

Unicode源於一個很簡單的想法:將全世界全部的字符包含在一個集合裏,計算機只要支持這一個字符集,就能顯示全部的字符,不再會有亂碼了。es6

它從0開始,爲每一個符號指定一個4個字節的編號,這叫作"碼點"(code point)。好比,碼點0的符號就是null(表示全部二進制位都是0),中文"好"的碼點是十六進制的597D正則表達式

U+0000 = null
U+597D = 好

上式中,U+表示緊跟在後面的十六進制數是`Unicode的碼點。函數

目前,Unicode的最新版本是10.0版,一共收入了136690個符號,這麼多符號,Unicode不是一次性定義的,而是分區定義。每一個區能夠存放65536個(216)字符,稱爲一個平面(plane),定義了17個平面,目前Unicode字符集的大小是1,114,112(17*216)。編碼

最前面的65536個字符位,稱爲基本平面(縮寫BMP,它的碼點範圍是從0一直到216-1,寫成16進制就是從U+0000U+FFFF。全部最多見的字符都放在這個平面,這是Unicode最早定義和公佈的一個平面。剩下的字符都放在輔助平面(縮寫SMP,碼點範圍從U+010000一直到U+10FFFFspa

16個輔助平面目前只用了6個:prototype

  • 第一輔助平面(SMP),擺放拼音文字(主要爲現時已再也不使用的文字)及符號。範圍在 U+10000 ~ U+1FFFD
  • 第二輔助平面(SIP),整個範圍在 U+20000 ~ U+2FFFD。現時擺放「中日韓統一表意文字擴展B區」,共43,253個漢字,以及中日韓兼容表意文字增補 (CJK Compatibility Ideographs Supplement)。
  • 第三 ~ 十三輔助平面,暫未使用。
  • 第十四輔助平面(SSP),擺放 Language tagsVariation Selectors ,它們都是控制字符。範圍在 U+E0000 ~ U+E01FF
  • 第十五 ~ 十六輔助平面都是私人使用區。它們的範圍是 U+F0000 ~ U+FFFFDU+100000 ~ U+1000FD

Unicode只是一個符號集,它只規定了符號的二進制代碼(碼點),卻沒有規定到底用什麼樣的字節序表示這個碼點,因此出現了不一樣的編碼方式---UTF-32,UTF-16,UTF-8設計

UTF-32與UTF-8

因爲每一個碼點爲4個字節,因此最直觀的編碼方法是使用4個字節表示,字節內容一一對應碼點。這種編碼方法就叫作UTF-32。好比,碼點0就用四個字節的0表示,碼點597D就在前面加兩個字節的0code

U+0000 = 0x0000 0000
U+597D = 0x0000 597D

clipboard.png

UTF-32的優勢在於,轉換規則簡單直觀,查找效率高。
缺點在於浪費空間,一樣內容的英語文本,它會比ASCII編碼大四倍。這個缺點很致命,致使實際上沒有人使用這種編碼方法,HTML5標準就明文規定,網頁不能編碼成UTF-32orm

clipboard.png

人們真正須要的是一種節省空間的編碼方法,這致使了UTF-8的誕生。UTF-8是一種變長的編碼方法,字符長度從1個字節到4個字節不等。越是經常使用的字符,字節越短,最前面的128個字符,只使用1個字節表示,與ASCII徹底相同

碼點範圍 字節數 可容納字符個數
0x0000 ~ 0x007F 1 128
0x0080 ~ 0x07FF 2 1920
0x0800 ~ 0xFFFF 3 63488
0x010000 ~ 0x10FFFF 4 1048575

因爲UTF-8這種節省空間的特性,致使它成爲互聯網上最多見的網頁編碼。

UTF-16

UTF-16編碼介於UTF-32UTF-8之間,同時結合了定長變長兩種編碼方法的特色。
它的編碼規則很簡單:

  • 基本平面的字符佔用2個字節;
  • 輔助平面的字符佔用4個字節。

也就是說,UTF-16的編碼長度要麼是2個字節(U+0000~U+FFFF),要麼是4個字節(U+010000~U+10FFFF)。

clipboard.png

因而就有一個問題,當咱們遇到兩個字節,怎麼看出它自己是一個字符,仍是須要跟其餘兩個字節放在一塊兒解讀?
說來很巧妙,不知道是否是故意的設計,在基本平面內,從U+D800~U+DFFF是一個空段,即這些碼點不對應任何字符。所以,這個空段能夠用來映射輔助平面的字符。
具體以下,先來計算一下輔助平面的碼點共有多少個:

$$17*2^{16} - 2^{16} = 2^{16} * 2^4 = 2^{20}$$

再計算一下須要多少個二進制位,220個碼點,意味着最後一個碼點對應於(從0開始因此要減1):
$$2^{20} - 1 $$

轉換爲16進制即是0xFFFFF,對應的二進制位數爲20位,也就是說,對應這些字符至少須要20個二進制位。

UTF-16將這20位拆成兩半,前10位映射在U+D800~U+DBFF(空間大小210),稱爲高位(H),後10位映射在U+DC00U+DFFF(空間大小210),稱爲低位(L)。這意味着,一個輔助平面的字符,被拆成兩個基本平面的字符表示

clipboard.png

因此,當咱們遇到兩個字節,發現它的碼點在U+D800~U+DBFF之間,就能夠判定,緊跟在後面的兩個字節的碼點,應該在U+DC00~U+DFFF之間,這四個字節必須放在一塊兒解讀

UTF-16的轉碼公式

Unicode碼點轉成UTF-16的時候,首先區分這是基本平面字符,仍是輔助平面字符。若是是前者,直接將碼點轉爲對應的十六進制形式,長度爲兩字節。

U+597D = 0x597D

若是是輔助平面字符,Unicode 3.0版給出了轉碼公式,對於碼點c

H = Math.floor((c - 0x10000) / 0x400) + 0xD800
L = (c - 0x10000) % 0x400 + 0xDC00

以字符?爲例,它是一個輔助平面字符,碼點爲U+20BB7,將其轉爲UTF-16的計算過程以下。

H = Math.floor((0x20BB7 - 0x10000) / 0x400) + 0xD800 = 0xD842
L = (0x20BB7 - 0x10000) % 0x400 + 0xDC00 = 0xDFB7

因此,?字符的UTF-16編碼就是0xD842DFB7,長度爲四個字節。

JavaScript使用哪種編碼?

JavaScript語言採用Unicode字符集,可是隻支持一種編碼方法。

clipboard.png

這種編碼既不是UTF-16,也不是UTF-8,更不是UTF-32。上面那些編碼方法,JavaScript都不用。JavaScript用的是UCS-2

clipboard.png

UCS-2編碼

怎麼忽然殺出一個UCS-2?這就須要講一點歷史。

互聯網還沒出現的年代,曾經有兩個團隊,不約而同想搞統一字符集。一個是1988年成立的Unicode團隊,另外一個是1989年成立的UCS團隊。等到他們發現了對方的存在,很快就達成一致:世界上不須要兩套統一字符集
199110月,兩個團隊決定合併字符集。也就是說,從今之後只發布一套字符集,就是Unicode,而且修訂此前發佈的字符集,UCS的碼點將與Unicode徹底一致

clipboard.png

UCS的開發進度快於Unicode1990年就公佈了第一套編碼方法UCS-2,使用2個字節表示已經有碼點的字符。(那個時候只有一個平面,就是基本平面,因此2個字節就夠用了。)。

UTF-16編碼遲至19967月才公佈,明確宣佈是UCS-2超集,即基本平面字符沿用UCS-2編碼,輔助平面字符定義了4個字節的表示方法。

二者的關係簡單說,就是UTF-16取代了UCS-2,或者說UCS-2整合進了UTF-16。因此,如今只有UTF-16,沒有UCS-2

JavaScript的誕生背景

那麼,爲何JavaScript不選擇更高級的UTF-16,而用了已經被淘汰的UCS-2呢?

答案很簡單:非不想也,是不能也。由於在JavaScript語言出現的時候,尚未UTF-16編碼。

19955月,Brendan Eich用了10天設計了JavaScript語言;10月,第一個解釋引擎問世;次年11月,Netscape正式向ECMA提交語言標準(整個過程詳見《JavaScript誕生記》)。對比UTF-16的發佈時間(19967月),就會明白Netscape公司那時沒有其餘選擇,只有UCS-2一種編碼方法可用!

clipboard.png

JavaScript字符函數的侷限

因爲JavaScript`只能處理UCS-2編碼,形成全部字符在這門語言中都是2個字節,若是是4個字節的字符,會看成兩個雙字節的字符處理。JavaScript的字符函數都受到這一點的影響,沒法返回正確結果

clipboard.png

仍是以?字符爲例,它的UTF-16編碼是4個字節的0xD842DFB7。問題就來了,4個字節的編碼不屬於UCS-2JavaScript不認識,只會把它看做單獨的兩個字符U+D842U+DFB7。前面說過,這兩個碼點是空的,因此JavaScript會認爲是兩個空字符組成的字符串

`?`.length //2
`?` === '\u20BB7' //false
`?`.charAt(0) // "�"
`?`.charCodeAt(0) // 55362(0xD842)

上面代碼表示,JavaScript認爲字符?的長度是2,取到的第一個字符是"�"字符,取到的第一個字符的碼點是0xD842。這些結果都不正確!

解決這個問題,必須對碼點作一個判斷,而後手動調整。下面是正確的遍歷字符串的寫法。

var index = -1;
var string = '?12';
var length = string.length;
var output = [];
while (++index < length) {
  var charCode = string.charCodeAt(index);
  var character = string.charAt(index);
  if (charCode >= 55296 && charCode <= 56319) {
    output.push(character + string.charAt(++index));
  } else {
    output.push(character);
  }
}
console.log(output) //["?", "1", "2"]

上面代碼表示,遍歷字符串的時候,必須對碼點作一個判斷,只要落在55296~56319(0xD800~0xDBFF)的區間,就要連同後面2個字節一塊兒讀取。

相似的問題存在於全部的JavaScript字符操做函數。

String.prototype.replace()
String.prototype.substring()
String.prototype.slice()
...

上面的函數都只對2字節的碼點有效。要正確處理4字節的碼點,就必須逐一部署本身的版本,判斷一下當前字符的碼點範圍。

ECMAScript 6

JavaScriptECMAScript 6版本(簡稱ES6),大幅加強了Unicode支持,基本上解決了這個問題。

  1. 正確識別字符
    ES6能夠自動識別4字節的碼點。所以,遍歷字符串就簡單多了。

    let s = '?12';
    let output = [];
    for(let s of string ){ 
        output.push(s)
    }
    console.log(output) //["?", "1", "2"]

    可是,爲了保持兼容,length屬性仍是原來的行爲方式。爲了獲得字符串的正確長度,能夠用下面的方式。

    Array.from(string).length
  2. 碼點表示法
    JavaScript一直容許直接用碼點表示Unicode字符,寫法是uxxxx形式,其中xxxx表示字符的Unicode 碼點。

    '好'==='\u597D' // true

    可是,這種表示法對4字節的碼點無效。ES6修正了這個問題,只要將碼點放在大括號內,就能正確識別。

    '?' === '\u20BB7' //false
    '?' === '\u{20BB7}' //true
  3. 字符串處理函數
    ES6新增了幾個專門處理4字節碼點的函數。

    • String.fromCodePoint():對應於String.fromCharCode(),從Unicode碼點返回對應字符
    • String.prototype.codePointAt():對應於String.prototype.charCodeAt(),從字符返回對應的Unicode碼點
    • String.prototype.at():對應於String.prototype.charAt(),返回字符串給定位置的字符
  4. 正則表達式
    ES6提供了u修飾符,含義爲Unicode模式,對正則表達式添加4字節碼點的支持。
  5. Unicode正規化
    有些字符除了字母之外,還有附加符號。好比,漢語拼音的Ǒ,字母上面的聲調就是附加符號。對於許多歐洲語言來講,聲調符號是很是重要的。

    Unicode提供了兩種表示方法,一種是帶附加符號的單個字符,即一個碼點表示一個字符,好比Ǒ的碼點是U+01D1;另外一種是將附加符號單獨做爲一個碼點,與主體字符複合顯示,即兩個碼點表示一個字符,好比Ǒ能夠寫成O(U+004F)+ˇ(U+030C)

    這兩種表示方法,視覺和語義都徹底同樣,理應做爲等同狀況處理。可是,JavaScript沒法辨別。

    '\u01D1'==='\u004F\u030C' //false

    ES6提供了normalize方法,容許"Unicode正規化",即將兩種方法轉爲一樣的序列。

    '\u01D1'.normalize()==='\u004F\u030C'.normalize() // true

參考連接

阮一峯--Unicode與JavaScript詳解
輔助平面
ECMAScript 6 入門

相關文章
相關標籤/搜索