本文大部份內容轉自 阮一峯前輩的文章,更新了部份內容並加入了部分本身的理解。html
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+0000
到U+FFFF
。全部最多見的字符都放在這個平面,這是Unicode
最早定義和公佈的一個平面。剩下的字符都放在輔助平面(縮寫SMP
),碼點範圍從U+010000
一直到U+10FFFF
。spa
16個輔助平面目前只用了6個:prototype
SMP
),擺放拼音文字(主要爲現時已再也不使用的文字)及符號。範圍在 U+10000
~ U+1FFFD
。SIP
),整個範圍在 U+20000
~ U+2FFFD
。現時擺放「中日韓統一表意文字擴展B
區」,共43,253
個漢字,以及中日韓兼容表意文字增補 (CJK Compatibility Ideographs Supplement
)。SSP
),擺放 Language tags
和 Variation Selectors
,它們都是控制字符。範圍在 U+E0000
~ U+E01FF
。U+F0000
~ U+FFFFD
及 U+100000
~ U+1000FD
。Unicode
只是一個符號集,它只規定了符號的二進制代碼(碼點),卻沒有規定到底用什麼樣的字節序表示這個碼點,因此出現了不一樣的編碼方式---UTF-32
,UTF-16
,UTF-8
設計
因爲每一個碼點爲4
個字節,因此最直觀的編碼方法是使用4
個字節表示,字節內容一一對應碼點。這種編碼方法就叫作UTF-32
。好比,碼點0
就用四個字節的0
表示,碼點597D
就在前面加兩個字節的0
。code
U+0000 = 0x0000 0000 U+597D = 0x0000 597D
UTF-32
的優勢在於,轉換規則簡單直觀,查找效率高。
缺點在於浪費空間,一樣內容的英語文本,它會比ASCII編碼大四倍。這個缺點很致命,致使實際上沒有人使用這種編碼方法,HTML5
標準就明文規定,網頁不能編碼成UTF-32
。orm
人們真正須要的是一種節省空間的編碼方法,這致使了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-32
與UTF-8
之間,同時結合了定長和變長兩種編碼方法的特色。
它的編碼規則很簡單:
也就是說,UTF-16
的編碼長度要麼是2
個字節(U+0000
~U+FFFF
),要麼是4
個字節(U+010000
~U+10FFFF
)。
因而就有一個問題,當咱們遇到兩個字節,怎麼看出它自己是一個字符,仍是須要跟其餘兩個字節放在一塊兒解讀?
說來很巧妙,不知道是否是故意的設計,在基本平面內,從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+DC00
到U+DFFF
(空間大小210),稱爲低位(L
)。這意味着,一個輔助平面的字符,被拆成兩個基本平面的字符表示。
因此,當咱們遇到兩個字節,發現它的碼點在U+D800
~U+DBFF
之間,就能夠判定,緊跟在後面的兩個字節的碼點,應該在U+DC00
~U+DFFF
之間,這四個字節必須放在一塊兒解讀。
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
語言採用Unicode
字符集,可是隻支持一種編碼方法。
這種編碼既不是UTF-16
,也不是UTF-8
,更不是UTF-32
。上面那些編碼方法,JavaScript
都不用。JavaScript
用的是UCS-2
!
怎麼忽然殺出一個UCS-2
?這就須要講一點歷史。
互聯網還沒出現的年代,曾經有兩個團隊,不約而同想搞統一字符集。一個是1988
年成立的Unicode
團隊,另外一個是1989
年成立的UCS
團隊。等到他們發現了對方的存在,很快就達成一致:世界上不須要兩套統一字符集。1991
年10
月,兩個團隊決定合併字符集。也就是說,從今之後只發布一套字符集,就是Unicode
,而且修訂此前發佈的字符集,UCS
的碼點將與Unicode
徹底一致。
UCS
的開發進度快於Unicode
,1990
年就公佈了第一套編碼方法UCS-2
,使用2
個字節表示已經有碼點的字符。(那個時候只有一個平面,就是基本平面,因此2
個字節就夠用了。)。
UTF-16
編碼遲至1996
年7
月才公佈,明確宣佈是UCS-2
的超集,即基本平面字符沿用UCS-2
編碼,輔助平面字符定義了4
個字節的表示方法。
二者的關係簡單說,就是UTF-16
取代了UCS-2
,或者說UCS-2
整合進了UTF-16
。因此,如今只有UTF-16
,沒有UCS-2
。
那麼,爲何JavaScript
不選擇更高級的UTF-16
,而用了已經被淘汰的UCS-2
呢?
答案很簡單:非不想也,是不能也。由於在JavaScript
語言出現的時候,尚未UTF-16
編碼。
1995
年5
月,Brendan Eich
用了10
天設計了JavaScript
語言;10
月,第一個解釋引擎問世;次年11
月,Netscape
正式向ECMA
提交語言標準(整個過程詳見《JavaScript誕生記》)。對比UTF-16
的發佈時間(1996
年7
月),就會明白Netscape
公司那時沒有其餘選擇,只有UCS-2
一種編碼方法可用!
因爲JavaScript
`只能處理UCS-2
編碼,形成全部字符在這門語言中都是2
個字節,若是是4
個字節的字符,會看成兩個雙字節的字符處理。JavaScript
的字符函數都受到這一點的影響,沒法返回正確結果。
仍是以?
字符爲例,它的UTF-16
編碼是4
個字節的0xD842DFB7
。問題就來了,4
個字節的編碼不屬於UCS-2
,JavaScript
不認識,只會把它看做單獨的兩個字符U+D842
和U+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
字節的碼點,就必須逐一部署本身的版本,判斷一下當前字符的碼點範圍。
JavaScript
的ECMAScript 6
版本(簡稱ES6
),大幅加強了Unicode
支持,基本上解決了這個問題。
正確識別字符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
碼點表示法JavaScript
一直容許直接用碼點表示Unicode
字符,寫法是uxxxx形式,其中xxxx表示字符的Unicode
碼點。
'好'==='\u597D' // true
可是,這種表示法對4
字節的碼點無效。ES6
修正了這個問題,只要將碼點放在大括號內,就能正確識別。
'?' === '\u20BB7' //false '?' === '\u{20BB7}' //true
字符串處理函數ES6
新增了幾個專門處理4
字節碼點的函數。
String.fromCodePoint()
:對應於String.fromCharCode()
,從Unicode
碼點返回對應字符String.prototype.codePointAt()
:對應於String.prototype.charCodeAt()
,從字符返回對應的Unicode
碼點String.prototype.at()
:對應於String.prototype.charAt()
,返回字符串給定位置的字符ES6
提供了u
修飾符,含義爲Unicode
模式,對正則表達式添加4
字節碼點的支持。Unicode
正規化
有些字符除了字母之外,還有附加符號。好比,漢語拼音的Ǒ
,字母上面的聲調就是附加符號。對於許多歐洲語言來講,聲調符號是很是重要的。
Unicode
提供了兩種表示方法,一種是帶附加符號的單個字符,即一個碼點表示一個字符,好比Ǒ
的碼點是U+01D1
;另外一種是將附加符號單獨做爲一個碼點,與主體字符複合顯示,即兩個碼點表示一個字符,好比Ǒ
能夠寫成O(U+004F)
+ˇ(U+030C)
。
這兩種表示方法,視覺和語義都徹底同樣,理應做爲等同狀況處理。可是,JavaScript
沒法辨別。
'\u01D1'==='\u004F\u030C' //false
ES6
提供了normalize
方法,容許"Unicode
正規化",即將兩種方法轉爲一樣的序列。
'\u01D1'.normalize()==='\u004F\u030C'.normalize() // true