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