以前看到ES6中對String擴展了很多新特性,字符串操做更加友好,好比"u{1f914}",codePointAt(),String.fromCodePoint()。其中涉及到很多字符編碼的知識,爲了更好理解這些新特性,本文對字符編碼相關知識作一個較全面的梳理和總結。javascript
如下內容包括:字符集和字符編碼的關係以及編碼規則,JS的字符編碼,HTML的轉義序列。html
首先,在前言裏面回顧一下位與字節(小b和大B)的最最基礎知識。前端
1bit = 1個二進制位 = 0 或 1java
8bit = 8個0或1(2^8=256個組合)= 1字節Bytegit
值得一提,在計算帶寬大小(bps)的時候要注意是以bit做爲單位。es6
Unicode、ASCII、GB23十二、GBK、BIG5都屬因而字符集(character set),每一個字符集包含的字符種類和數量都不同,每一個字符有各自的編號做爲惟一標識。github
那麼它們是經過什麼方式進行編號(如下都稱爲碼點)的呢?不一樣的字符集有不一樣的方案,對於ASCII、GB23十二、GBK、BIG5來講,實行「壟斷」政策,即只容許使用它規定的編碼方案,也能夠認爲它便是字符集也是字符編碼。而Unicode實行「百家爭鳴」政策,提供了UTF-8/UTF-16/UTF-32幾種備選的字符編碼方案,因此這時Unicode僅僅是字符集,UTF-X纔是字符編碼。windows
各個字符集的具體編碼方案能夠看這裏瀏覽器
正由於這個緣由,常常會聽到說ASCII編碼、GB2312編碼,甚至Unicode編碼,這種叫法很容易混淆字符集和字符編碼的關係。app
弄清楚字符集與字符編碼的關係以後,咱們能夠知道若是某個字符想從UTF-8編碼轉成GBK編碼的話,那就必須先將其unicode碼點換算成GBK碼點,再進行GBK編碼。
下面咱們主要看看ASCII和Unicode這兩種字符集(編碼)。
ASCII是最古老原始的字符集和編碼,主要是知足英語字符的須要,畢竟計算機是從人家老美那誕生的。
每一個字符用一個字節(8bit)來儲存,一共定義了128個字符,前32個字符是非打印控制字符(回車換行等)。雖然一個字節最多能夠定義256種字符,可是ASCII只用了1個字節的後面7位,最前面統一都爲0。
空格"SPACE"碼點:十進制32,十六進制20,二進制00100000
大寫的字母A碼點:十進制65,十六進制41,二進制01000001
ASCII只有128個字符,其餘語言不夠用了,怎麼辦?
別忘了ASCII只用了後面7位,利用空閒的最高位,這樣能夠擴容到256個字符,成爲擴展ASCII碼(EASCII)。因此0-127碼點表示的字符是同樣的,不同的只是128-255碼段。
因爲只能多擴容128個字符,而各國語言中的字符又各不相同,爲了知足不一樣地區的更多字符的需求,因此擴容字符的含義不可能都同樣。這裏就會出現如ASCII碼錶「阿拉伯字符(ASMO-708)碼」擴展ASCII,「泰語(Windows)碼」擴展ASCII。
而對於中文而言,1個字節256個字符顯然不夠,所以中文只能單獨制定如GB23十二、GBK、GB18030、BIG5字符集了。關於GBXXX編碼能夠看這裏。
看到這裏,有種貴圈真亂的感受,各國都自行一套字符集及編碼,這就不利於溝通交流阿。直到Unicode出現。
Unicode解決了各國自行一套的問題,將世界上全部的符號都歸入其中。它符提供了惟一碼點,不管是什麼平臺、不管是什麼程序、不管是什麼語言。
碼點code point範圍從 0x0 - 0x10FFFF,共分爲17個Plane,每一個Plane中有65536個字符,共可容納: 17*(16*16*16*16)= 1114112
個字符。
第一個平面稱爲基本多語言平面(Basic Multilingual Plane, BMP)。其餘平面稱爲輔助平面(Supplementary Planes, SP),或astral Plane。
<u>BMP內,從U+D800到U+DFFF之間的碼位區塊是永久保留不映射到Unicode字符</u>。後面介紹的UTF-16就利用保留下來的0xD800-0xDFFF區段的碼位來對輔助平面的字符的碼位進行編碼。
前面說到Unicode只是字符集,具體碼點怎麼儲存,可選擇UTF-八、UTF-16或者UTF-32編碼方式。
這裏插播一個名詞:code units 碼元
碼元是各編碼方式的基本單位,長度單位是bit。一個碼點有可能只須要一個碼元,也有可能須要多個碼元。
UTF-x等編碼方式中的數字其實就規定了此編碼方式下的碼元長度。如UTF-8的碼元長度爲8bit.......
當一個碼點太大,一碼元長度無法儲存時,這時就須要其分解成兩個或以上碼元來儲存。
如0x10437碼點UTF-16會分解成D801 DC37兩個碼元(每一個碼元16bit),UTF-8會分解成f0 90 90 b7四個碼元(每一個碼元8bit)
是在互聯網上使用最廣的一種Unicode的實現方式,
是一種變長的編碼方式。可使用1~4個字節存儲一個字符,根據不一樣的符號而變化字節長度。
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)
從上面D801 DC37的例子能夠發現,這兩個碼點都落在了BMP平面的碼點範圍以內,<u>而且都屬於U+D800到U+DFFF碼段</u>。沒錯,就是經過一系列規則,把超出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<u>減去0x10000</u>,結果爲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('\ud83e\udd14'); // ?
在沒有輔助平面字符前,UTF-16與UCS-2所指的是同一的意思。但當引入輔助平面字符後,就稱爲UTF-16了。也就是說,UCS-2編碼不能支持在UTF-16中超過2字節的字集。
阮老師的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腳本的?
引擎會把全部源碼當作是一連串的UTF16碼元,也就是內部是以UTF-16進行編碼的。
var f\u006F\u006F = 123; console.log(foo); // 123 console.log(\u0066\u006F\u006F); // 123 var foo = '12\u0033'; // 123 // 中文 var 騰 = '123'; console.log(\u817e); // 123 // 4字節字符 var bar = '?'; console.log('\uD842\uDFB7'); // '?'
上面的例子能夠看到,不管是字符串仍是變量,不管是BMP仍是SP上的字符,均可以使用UTF-16碼元來表示。
那ES6中的大括號表示法呢?看起來並不須要UTF-16編碼,直接用大括號包裹碼點就行了。
'\u{20BB7}' === '\uD842\uDFB7' // 居然全等
但實際上只是語法糖,而這個語法糖很贊,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,和上面的八進制表示目的是同樣的
其實瞭解了上面的知識之後,對於字符串的length就不難理解了。
對於JS引擎來講,全部的字符串都是一系列的UTF-16碼元,length指的是碼元的個數(也能夠理解爲兩個字節等於1個length),而不是字符個數。當某個字符是4個字節的UTF-16編碼時,這時一個字符的length就爲2。可是中文的length卻始終爲1,這是由於中文的碼點範圍U+4E00 - U+9FA5,都在BMP平面內,UTF-16編碼只須要2個字節。
來看例子~
// 仍是拿這個字 '?' === '\uD842\uDFB7'; '\uD842\uDFB7' === '\u{20BB7}'; var foo = '?'; var bar = '\uD842\uDFB7'; 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('?????'); // 5
咱們能夠以不一樣的編碼方式來保存源碼,但若是瀏覽器解碼方案和源碼保存時的編碼方案不一樣,就會致使亂碼。
當瀏覽器在加載一個<script>時,是經過如下優先級來肯定其編碼方式:
若是文件開頭有BOM(byte order mark),那麼它確定是UTF編碼的其中之一,而又由於不一樣編碼下的BOM不同,因此能夠從BOM來肯定編碼方式
經過響應頭Content-Type: application/javascript; charset=utf-8
<script>標籤中的charset屬性
頁面中的<meta charset="UTF-8">
不一樣編碼下的BOM
BOM(U+FEFF) 編碼方式 00 00 FE FF UTF-32, big-endian FF FE 00 00 UTF-32, little-endian FE FF UTF-16, big-endian FF FE UTF-16, little-endian EF BB BF UTF-8
最後來總結一下前端常常能看到如
,中
這樣的轉義字符。它們是HTML、XML等 SGML 類語言的轉義序列(escape sequence),瀏覽器遇到他們會自動渲染成文字。
它一共有三種表達方式:
dddd; 後接十進制數——稱之爲實體編號
hhhh; 後接十六進制數——稱之爲實體編號
&name; &後接預先定義好的實體名稱——稱之爲實體名稱
其中前兩種方式的數字取值爲目標字符的unicode碼點,與文檔編碼無關。
字符實體 | 實體編號 | 實體名稱 |
---|---|---|
中國 | 中国 或 中国 |
無 |
& | & |
& |
對於實體編號,可使用ES6中的String.fromCodePoint(codePoint)
String.fromCodePoint(20013); // 中 String.fromCodePoint(0x4e2d); // 中
或者藉助dom
var $dom = $('<div>中国</div>'); $dom.html(); // 中國
自此,對字符編碼有了一個較爲全面的瞭解,這時回過頭去看那些String新特性時,更容易理解了,印象深入了很多。原本還有一些關於emoji表情的內容,仍是後面另起一篇再說吧。