摘要:字符編碼的那些事前言之前看到中對擴展了不少新特性,字符串操作更加友好,比如,,。其中涉及到不少字符編碼的知識,為了更好理解這些新特性,本文對字符編碼相關知識做一個較全面的梳理和總結。
字符編碼的那些事 前言
之前看到ES6中對String擴展了不少新特性,字符串操作更加友好,比如"u{1f914}",codePointAt(),String.fromCodePoint()。其中涉及到不少字符編碼的知識,為了更好理解這些新特性,本文對字符編碼相關知識做一個較全面的梳理和總結。
以下內容包括:字符集和字符編碼的關系以及編碼規則,JS的字符編碼,HTML的轉義序列。
首先,在前言里面回顧一下位與字節(小b和大B)的最最基礎知識。
1bit = 1個二進制位 = 0 或 1
8bit = 8個0或1(2^8=256個組合)= 1字節Byte
值得一提,在計算帶寬大小(bps)的時候要注意是以bit作為單位。
一、字符集與字符編碼Unicode、ASCII、GB2312、GBK、BIG5都屬于是字符集(character set),每個字符集包含的字符種類和數量都不一樣,每個字符有各自的編號作為唯一標識。
那么它們是通過什么方式進行編號(以下都稱為碼點)的呢?不同的字符集有不同的方案,對于ASCII、GB2312、GBK、BIG5來說,實行“壟斷”政策,即只允許使用它規定的編碼方案,也可以認為它即是字符集也是字符編碼。而Unicode實行“百家爭鳴”政策,提供了UTF-8/UTF-16/UTF-32幾種備選的字符編碼方案,所以這時Unicode僅僅是字符集,UTF-X才是字符編碼。
各個字符集的具體編碼方案可以看這里
正因為這個原因,經常會聽到說ASCII編碼、GB2312編碼,甚至Unicode編碼,這種叫法很容易混淆字符集和字符編碼的關系。
弄清楚字符集與字符編碼的關系之后,我們可以知道如果某個字符想從UTF-8編碼轉成GBK編碼的話,那就必須先將其unicode碼點換算成GBK碼點,再進行GBK編碼。
下面我們主要看看ASCII和Unicode這兩種字符集(編碼)。
二、ASCII字符集及編碼ASCII是最古老原始的字符集和編碼,主要是滿足英語字符的需要,畢竟計算機是從人家老美那誕生的。
每個字符用一個字節(8bit)來儲存,一共定義了128個字符,前32個字符是非打印控制字符(回車換行等)。雖然一個字節最多可以定義256種字符,但是ASCII只用了1個字節的后面7位,最前面統一都為0。
空格"SPACE"碼點:十進制32,十六進制20,二進制00100000
大寫的字母A碼點:十進制65,十六進制41,二進制01000001
Extended ASCIIASCII只有128個字符,其他語言不夠用了,怎么辦?
別忘了ASCII只用了后面7位,利用空閑的最高位,這樣可以擴容到256個字符,成為擴展ASCII碼(EASCII)。所以0-127碼點表示的字符是一樣的,不一樣的只是128-255碼段。
由于只能多擴容128個字符,而各國語言中的字符又各不相同,為了滿足不同地區的更多字符的需求,所以擴容字符的含義不可能都一樣。這里就會出現如ASCII碼表“阿拉伯字符(ASMO-708)碼”擴展ASCII,“泰語(Windows)碼”擴展ASCII。
而對于中文而言,1個字節256個字符顯然不夠,因此中文只能多帶帶制定如GB2312、GBK、GB18030、BIG5字符集了。關于GBXXX編碼可以看這里。
ASCII碼表
看到這里,有種貴圈真亂的感覺,各國都自行一套字符集及編碼,這就不利于溝通交流阿。直到Unicode出現。
三、UnicodeUnicode解決了各國自行一套的問題,將世界上所有的符號都納入其中。它符提供了唯一碼點,不論是什么平臺、不論是什么程序、不論是什么語言。
碼點code point范圍從 0x0 - 0x10FFFF,共分為17個Plane,每個Plane中有65536個字符,共可容納: 17*(16*16*16*16)= 1114112 個字符。
第一個平面稱為基本多語言平面(Basic Multilingual Plane, BMP)。其他平面稱為輔助平面(Supplementary Planes, SP),或astral Plane。
BMP內,從U+D800到U+DFFF之間的碼位區塊是永久保留不映射到Unicode字符。后面介紹的UTF-16就利用保留下來的0xD800-0xDFFF區段的碼位來對輔助平面的字符的碼位進行編碼。
前面說到Unicode只是字符集,具體碼點怎么儲存,可選擇UTF-8、UTF-16或者UTF-32編碼方式。
1. UTF-8這里插播一個名詞:code units 碼元
碼元是各編碼方式的基本單位,長度單位是bit。一個碼點有可能只需要一個碼元,也有可能需要多個碼元。
UTF-x等編碼方式中的數字其實就規定了此編碼方式下的碼元長度。如UTF-8的碼元長度為8bit.......
當一個碼點太大,一碼元長度沒法儲存時,這時就需要其分解成兩個或以上碼元來儲存。
如0x10437碼點UTF-16會分解成D801 DC37兩個碼元(每個碼元16bit),UTF-8會分解成f0 90 90 b7四個碼元(每個碼元8bit)
中日韓漢字unicode編碼表
Unicode code converter
是在互聯網上使用最廣的一種Unicode的實現方式,
是一種變長的編碼方式。可以使用1~4個字節存儲一個字符,根據不同的符號而變化字節長度。
UTF-8的編碼規則Unicode碼點范圍 | UTF-8編碼方式(二進制) | UTF-8編碼所需大小 | 規則 | 備注 |
---|---|---|---|---|
U+0000 0000 - U+0000 007F | 0xxxxxxx | 1byte | 字節的第一位設為0;后面7位為這個符號的unicode碼。 | 英語字母,UTF-8編碼和ASCII碼是相同的 |
U+0000 0080 - U+0000 07FF | 110xxxxx 10xxxxxx | 2byte | 第一個字節前兩位是1,第三位是0;后面字節的前兩位一律設為10 | |
U+0000 0800 - U+0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx | 3byte | 第一個字節前三位是1,第四位是0;后面字節的前兩位一律設為10 | 漢字(U+4E00 - U+9FA5)在UTF-8里的編碼都是 3 個字節 |
U+0001 0000 - U+0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 4byte | 第一個字節前四位是1,第五位是0;后面字節的前兩位一律設為10 |
2字節:從0x0 - 0xFFFF的碼段(BMP),編碼后的數值和unicode對應的碼點一致
4字節(兩個雙字節):從0x10000 - 0x10FFFF的碼點(SP,已經超過了BMP平面),會根據規則,編碼成一對16bit長的碼元:如0x10437碼點會編碼成D801 DC37,它們叫做代理對(surrogate pair)
4字節代理對的原理從上面D801 DC37的例子可以發現,這兩個碼點都落在了BMP平面的碼點范圍之內,并且都屬于U+D800到U+DFFF碼段。沒錯,就是通過一系列規則,把超出BMP平面的碼點(U+10437)轉換成兩個屬于BMP平面的碼點——U+D800到U+DFFF碼段之間(U+D801和U+DC37)。
SP平面中的碼點范圍是從U+10000到U+10FFFF,共計FFFFF個,即2^20=1,048,576個,需要20位來表示。
如果用兩個雙字節長的碼點組成的序列來表示,第一個碼點(稱為高位代理)要容納上述20位的前10位,第二個碼點(稱為低位代理)容納上述20位的后10位。前后分別需要2^10=1024個碼點來代理。而BMP平面的U+D800到U+DFFF碼段正好有2048個碼點,足以滿足高位代理與低位代理的需要。
因此需要將U+D800 - U+DFFF分為兩段
一段為高位代理初始值U+D800:U+D800 到 U+DBFF 之間的保留碼點用于高位代理(leading surrogates)
一段為低位代理初始值U+DC00:U+DC00 到 U+DFFF 之間的保留碼點則用于低位代理(trailing surrogates)
兩段之間間隔2^10=1024,剛好各自能夠滿足前后10位
0x10437減去0x10000,結果為0x00437,二進制為0000 0000 0100 0011 0111
分區它的上10位值和下10位值(使用二進制):0000000001 and 0000110111
添加0xD800到上值,以形成高位:0xD800 + 0x0001 = 0xD801。
添加0xDC00到下值,以形成低位:0xDC00 + 0x0037 = 0xDC37。
得出它的UTF-16編碼為D801 DC37
function findSurrogatesPair(codePoint) { var offset = codePoint - 0x10000; var lead = 0xd800 + (offset >> 10); var tail = 0xdc00 + (offset & 0x3ff); // 和1023 位與 把前十位都置為0 return [lead.toString(16) + tail.toString(16)]; } // example var str = "?"; var cp = str.codePointAt(0); utf16Encode(cp); // return ["d83e", "dd14"] console.log("ud83eudd14"); // ?UTF-16是UCS-2的父集
在沒有輔助平面字符前,UTF-16與UCS-2所指的是同一的意思。但當引入輔助平面字符后,就稱為UTF-16了。也就是說,UCS-2編碼不能支持在UTF-16中超過2字節的字集。
四、JS字符編碼阮老師的ES6教程字符串的擴展里面的第一小節字符的unicode表示法中提到:
......
有了這種表示法之后,JavaScript 共有6種方法可以表示一個字符。
"z" === "z" // true "172" === "z" // true "x7A" === "z" // true "u007A" === "z" // true "u{7A}" === "z" // true
這里面的u007A看起來JS好像是UTF-16編碼,但是平時加載一個JS時指定的charset又好像是UTF-8或者GBK?那JS到底是以什么來編碼的?
這個問題我一直都有點懵逼,但實際上對于JS的編碼問題應該分成兩個不同的部分看待:
內部:JS引擎是如何解析的?
外部:瀏覽器是以什么編碼來解析JS腳本的?
1. 內部:JS引擎解析源碼引擎會把所有源碼當做是一連串的UTF16碼元,也就是內部是以UTF-16進行編碼的。
var fu006Fu006F = 123; console.log(foo); // 123 console.log(u0066u006Fu006F); // 123 var foo = "12u0033"; // 123 // 中文 var 騰 = "123"; console.log(u817e); // 123 // 4字節字符 var bar = "?"; console.log("uD842uDFB7"); // "?"
上面的例子可以看到,無論是字符串還是變量,無論是BMP還是SP上的字符,都可以使用UTF-16碼元來表示。
那ES6中的大括號表示法呢?看起來并不需要UTF-16編碼,直接用大括號包裹碼點就好了。
"u{20BB7}" === "uD842uDFB7" // 竟然全等
但實際上只是語法糖,而這個語法糖很贊,ES6內部對大括號內的碼點進行了UTF-16編碼,不需要自己換算成代理對。
此外,上面還有三種表示法看起來怪怪的
"z" === "z" // true "172" === "z" // true 八進制 "x7A" === "z" // true 十六進制
z實際上是用于轉義特殊字符,如r n t " "等,而z這種非特殊含義字符則等于它本身
八進制表示法,反斜杠后的取值范圍是0-377(十進制的0-255),官方說法是用來表示Latin-1編碼字符
十六進制表示法,取值范圍是00-FF,和上面的八進制表示目的是一樣的
迷之String.prototype.length其實了解了上面的知識以后,對于字符串的length就不難理解了。
對于JS引擎來說,所有的字符串都是一系列的UTF-16碼元,length指的是碼元的個數(也可以理解為兩個字節等于1個length),而不是字符個數。當某個字符是4個字節的UTF-16編碼時,這時一個字符的length就為2。但是中文的length卻始終為1,這是因為中文的碼點范圍U+4E00 - U+9FA5,都在BMP平面內,UTF-16編碼只需要2個字節。
來看例子~
// 還是拿這個字 "?" === "uD842uDFB7"; "uD842uDFB7" === "u{20BB7}"; var foo = "?"; var bar = "uD842uDFB7"; var baz = "u{20BB7}"; console.log(foo.length, bar.length, baz.length); // 2 2 2
而我們需要獲取“正確”的length值該怎么辦?
ES6輕松解決:Array.from(str).length
不是ES6也不要灰心,只要識別兩個相鄰的碼元是否形成代理對的關系(原理在上面有講~),是的話把它們視為一個整體。
function getRealLen(str) { var reg = /[ud800-udbff][udc00-udfff]/g; // /[高位代理][低位代理]/g return str.replace(reg, "i").length; } getRealLen("?"); // 1 getRealLen("?????"); // 52. 外部:瀏覽器解析JS腳本
我們可以以不同的編碼方式來保存源碼,但如果瀏覽器解碼方案和源碼保存時的編碼方案不同,就會導致亂碼。
當瀏覽器在加載一個