聊聊前端字符編碼:ASCII、Unicode、Base64、UTF-8、UTF-16、UTF-32
大家好,我是 CUGGZ。
在開發(fā)過程中經(jīng)常會遇到各種各樣的編碼,常見的有 UTF-8、Unicode、Base64 等,但前端世界遠不止這三種編碼,本文就來介紹前端常見的編碼以及其使用方式。
ASCII
我們知道,計算機只能理解二進制,二進制語言是面向機器的語言,直接來自計算機的指令系統(tǒng),由 0 和 1 組成。它使用整數(shù)來編碼數(shù)字(0-9)、大寫字母(A-Z)、小寫字母(A-Z)以及分號(;)、感嘆號(!)等。例如,97 用于表示“a”,33用于表示“!”,這樣就可以方便地存儲在內(nèi)存中。
互聯(lián)網(wǎng)的早期只有英文字母,所以不需要擔心任何其他字符,ASCII 就可以適用于這種情況的字符編碼,例如 bits 對應的二進制如下:
ASCII 全稱為 American Standard Code for Information Interchange,即“美國信息交換標準代碼”,是基于拉丁字母的一套電腦編碼系統(tǒng)。ASCII 至今為止共定義了 128 個字符:
ASCII 可以分為兩類:
- 可顯示字符:編號范圍是32-126(0x20-0x7E),共 95 個字符:
- 控制字符:編號范圍是0-31和127(0x00-0x1F和0x7F),共 33 個字符:
可以看到,ASCII 碼實際上是一種映射,是從二進制字符到字母數(shù)字字符的映射。所以當計算機收到以下二進制文件時:
使用 ASCII 碼進行映射,上面的二進制編碼可以翻譯成“Hello world”。
“K” 在ASCII 中是75,可以將它轉化為二進制,將 75 除以 2,然后繼續(xù),直到得到 0。如果除法不準確,則加 1 作為余數(shù):
現(xiàn)在,提取“余數(shù)”并以相反的順序放入它們:
因此,在 ASCII 中,“K”在二進制中被編碼為 1001011。
ASCII 的主要缺點是它只能表示 256 個不同的字符,因為它只能使用 8 位。ASCII 不能用于對世界各地發(fā)現(xiàn)的許多類型的字符進行編碼。但是如果想在計算機上使用中文、俄語、日語時,就需要一個不同的編碼標準。Unicode 進一步擴展為 UTF-8、UTF-16、UTF-32以對各種類型的字符進行編碼。因此,ASCII 和 Unicode 之間的主要區(qū)別就是用于編碼的位數(shù)。下面就來看看 Unicode 的概念以及使用方式。
Unicode
Unicode 是另一種字符編碼,它仍然是:位查找 -> 字符,由 Unicode Consortium 維護,其負責制定國際使用的軟件標準。IT 行業(yè)將 Unicode 標準化以對計算機和其他電子和通信設備中的字符進行編碼和表示。
Unicode 由許多代碼點組成(將來自世界各地的大量字符映射到所有計算機都可以引用的鍵),代碼點的集合稱為字符集,這就是 Unicode。開發(fā) Unicode 的目標是通過一種獨特的方式將世界上任何語言的任何字符或符號轉換成唯一的數(shù)字。可以在 unicode.org 上查找任何 Unicode 字符的編號,包括表情符號!
Unicode 看起來像這樣:
可以使用以下格式在 JavaScript 字符串中添加 unicode 序列 \uXXXX:
可以通過組合兩個 unicode 序列來創(chuàng)建一個序列:
雖然兩個字符串的結果都是 e?,但它們是兩個不同的字符串,并且長度也不相同:
ASCII 和 Unicode 是兩種流行的編碼方案。ASCII 編碼符號、數(shù)字、字母等,而 Unicode 編碼來自不同語言、字母、符號等的特殊文本,可以說 ASCII 是 Unicode 編碼方案的一個子集。它們兩個的區(qū)別如下:
UTF-8、UTF-16、UTF-32
(1)基本概念
UTF 是 Unicode 編碼方式的一種。UTF 編碼由 Unicode 標準定義,能夠對需要的每個 Unicode 代碼點進行編碼。Unicode 編碼方案根據(jù)用于對字符進行編碼的位數(shù)進行分類。目前使用的 Unicode 編碼方案有 UTF-7、UTF-8、UTF-16 和 UTF-32 ,分別使用 7 位、8 位、16 位和 32 位來表示字符。
那如何知道文件將使用哪種編碼呢?有一種稱為字節(jié)順序標記(BOM,即 Byte Order Mark) 的東西,也稱為編碼簽名。BOM是文件開頭的一個兩字節(jié)標記,用于標識文件是采用哪種格式的編碼。
UTF-8 在互聯(lián)網(wǎng)上使用最多,在 HTML5 中也被指定為文檔的首選編碼,所以下面將主要介紹 UTF-8。
UTF-8 將 0-127 的所有 Unicode 代碼點編碼為 1 個字節(jié)(與ASCII相同)。這意味著如果使用 ASCII 對程序進行編碼,而用戶使用 UTF-8,將不會有任何錯誤。1993年創(chuàng)建 UTF-8 時,很多數(shù)據(jù)都是 ASCII 格式的,所以通過兼容 UTF-8,在使用前不需要對數(shù)據(jù)進行轉換。本質上,ASCII 格式的文件可以被視為 UTF-8 格式,而且可以正常工作。
(2)工作原理
下面來詳細看看 UTF-8 是如何工作的,以及為什么它會根據(jù)被編碼的字符具有不同的長度。
UTF-8 以動態(tài)方式存儲數(shù)字。Unicode 列表中的第一個占用 1 個字節(jié),最后一個最多占用 4 個字節(jié),如果處理的是英文文件,大多數(shù)字符可能只占用 1 個字節(jié),與 ASCII 中的相同。這是通過用不同的字節(jié)數(shù)覆蓋 Unicode 中的不同范圍來實現(xiàn)的。
例如,要編碼原始 ASCII 表中的字符(0-127),只需要 7 位,因為 27= 128。因此,可以將所有內(nèi)容存儲在 8 位的 1 字節(jié)中,并且仍然剩余一個空閑空間。而對于下一個范圍(128-2047),就需要 11 bits,因為211=2048,在 UTF-8 中就是 2 個字節(jié)。
計算機在讀取 UTF-8 中以 0 開頭的內(nèi)容時,就知道只需要讀取一個字節(jié)并顯示 Unicode 中 0-127 范圍內(nèi)的正確字符即可。如果遇到兩個 1,就需要讀取 2 個字節(jié),范圍為128-2047,3 個 1 在一起表示需要讀取三個字節(jié)。
(3)UTF-16、UTF-32
世界范圍內(nèi)使用的大量字符是無法全部使用 8 位表示法進行編碼,導致在 Unicode 編碼下產(chǎn)生了 UTF-16 和 UTF-32 編碼格式。在解釋這些編碼格式之前,先來看看平面的概念:
Unicode 編碼中有很多很多的字符,它并不是一次性定義的,而是分區(qū)進行定義的,每個區(qū)存放65536(216)個字符,這稱為一個平面,目前總共有17 個平面。最前面的一個平面稱為基本平面,它的碼點從0 — 216-1,寫成16進制就是U+0000 — U+FFFF,那剩下的16個平面就是輔助平面,碼點范圍是 U+10000—U+10FFFF。
UTF-16 是 Unicode 編碼集的一種編碼形式,把 Unicode 字符集的抽象碼位映射為16位長的整數(shù)(即碼元)的序列,用于數(shù)據(jù)存儲或傳遞。Unicode字符的碼位需要1個或者2個16位長的碼元來表示,因此UTF-16也是用變長字節(jié)表示的。
UTF-16 編碼規(guī)則:
- 編號在 U+0000—U+FFFF 的字符(常用字符集),直接用兩個字節(jié)表示。
- 編號在 U+10000—U+10FFFF 之間的字符,需要用四個字節(jié)表示。
那么問題來了,當遇到兩個字節(jié)時,怎么知道是把它當做一個字符還是和后面的兩個字節(jié)一起當做一個字符呢?
UTF-16 編碼肯定也考慮到了這個問題,在基本平面內(nèi),從 U+D800 — U+DFFF 是一個空段,也就是說這個區(qū)間的碼點不對應任何的字符,因此這些空段就可以用來映射輔助平面的字符。
輔助平面共有 220 個字符位,因此表示這些字符至少需要 20 個二進制位。UTF-16 將這 20 個二進制位分成兩半,前 10 位映射在 U+D800 — U+DBFF,稱為高位(H),后 10 位映射在 U+DC00 — U+DFFF,稱為低位(L)。這就相當于,將一個輔助平面的字符拆成了兩個基本平面的字符來表示。
因此,當我們遇到兩個字節(jié)時,發(fā)現(xiàn)它的碼點在 U+D800 —U+DBFF之間,就可以知道,它后面的兩個字節(jié)的碼點應該在 U+DC00 — U+DFFF 之間,這四個字節(jié)必須放在一起進行解讀。
以 “??” 字為例,它的 Unicode 碼點為 0x21800,該碼點超出了基本平面的范圍,因此需要用四個字節(jié)來表示,步驟如下:
- 首先計算超出部分的結果:0x21800 - 0x10000
- 將上面的計算結果轉為20位的二進制數(shù),不足20位就在前面補0,結果為:0001000110 0000000000
- 將得到的兩個10位二進制數(shù)分別對應到兩個區(qū)間中
- U+D800 對應的二進制數(shù)為 1101100000000000, 將0001000110填充在它的后10 個二進制位,得到 1101100001000110,轉成 16 進制數(shù)為 0xD846。同理,低位為 0xDC00,所以這個字的UTF-16 編碼為 0xD846 0xDC00
UTF-32 就是字符所對應編號的整數(shù)二進制形式,每個字符占四個字節(jié),這個是直接進行轉換的。該編碼方式占用的儲存空間較多,所以使用較少。比如“馬” 字的 Unicode 編號是:U+9A6C,整數(shù)編號是39532,直接轉化為二進制:1001 1010 0110 1100,這就是它的 UTF-32 編碼。
(4)指定編碼方式
如果沒有顯式指定編碼方式,瀏覽器假定任何程序的源代碼都是用本地字符集編寫的,這會因國家/地區(qū)而異,可能會出現(xiàn)意料之外的情況。因此,給 JavaScript 文檔設置字符集非常重要。那該如何指定 UTF 編碼呢?
如果使用 HTTP(或 HTTPS)獲取文件,則 Content-Type 標頭可以指定編碼標準:
如果沒有設置,可以檢查 script? 標簽的 charset 屬性:
如果未設置,可以將它嵌入到 <head> 的頂部。
注意,這兩種情況下的 charset 屬性都不區(qū)分大小。
雖然 JavaScript 源文件可以是任何類型的編碼,但 JavaScript 會在執(zhí)行之前在內(nèi)部將其轉換為 UTF-16。正如 ECMAScript 標準所說,JavaScript 字符串都是 UTF-16 序列:
當 String 包含實際文本數(shù)據(jù)時,每個元素都被視為單個 UTF-16 代碼單元。
Base64
(1)基本概念
Base64 也稱為 Base64 內(nèi)容傳輸編碼。Base64 是將二進制數(shù)據(jù)編碼為 ASCII 文本。但它只使用了 64 個字符,再加上大多數(shù)字符集中存在的一個填充字符。所以它是一種僅使用可打印字符表示二進制數(shù)據(jù)的方法。Base64 常用于在通常處理文本數(shù)據(jù)的場景,表示、傳輸、存儲一些二進制數(shù)據(jù),包括MIME的電子郵件及XML的一些復雜數(shù)據(jù)。
Base64 編碼用于通過不能正確處理二進制數(shù)據(jù)的介質傳輸數(shù)據(jù)。因此,對數(shù)據(jù)進行 Base64 編碼以確保數(shù)據(jù)完整,無需通過此介質進行任何修改。base 64 編碼結果形式如下:
(2)Base64 編碼
Base64 編碼會將每 3 個字節(jié)的數(shù)據(jù)翻譯成 4 個編碼字符。它將從左到右開始掃描,然后選擇代表 3 個字符的數(shù)據(jù)的前 3 個字節(jié)。這 3 個字節(jié)將是 24 位。現(xiàn)在它將把這 24 位分成四部分,每部分 6 位。然后每個 6 位組將在下表中進行索引以得到映射的字符:
比如,有以下字符串:
它的位表示將是:
這些總共 24 位將被分為 4 組,每組 6 位:
它的數(shù)字表示為:
使用上面的數(shù)字索引到 base64 表中,映射結果如下:
- 24 → Y
- 22 → W
- 9 → J
- 0 → A
所以,這個字符串的base64編碼就是:
那如果輸入的字符串不是 3 的倍數(shù)怎么辦?這種情況下,就會使用填充字符??=?
?。假設字符串有 4 個字符
它的位表示將是:
前三個字節(jié)將組合在一起。最后的字節(jié)將用 4 個額外的 0 填充,以使總位可以被 6 整除:
使用上面的數(shù)字索引到 base64 表中,映射結果如下:
- 24 → Y
- 22 → W
- 9 → J
- 0 → A
- 24 → Y
- 48 → w
結果如下:
這里,每兩個額外的 0 由 = 字符表示。由于添加了 4 個額外的零,因此最后有兩個 =。
(3)Base64 解碼
說完了 Base64 編碼,下面來嘗試將 base64 編碼的字符串解碼為原始字符串。以以下 Base64 字符串為例:
將其分組為 4 個字符一組:
現(xiàn)在從每個組中刪除最后的 = 字符。對于剩余的字符串,將其轉換為上表中相應的位表示形式。
現(xiàn)在將其分組為一組 8 位,保留尾隨的 0:
現(xiàn)在對于上面的每個字節(jié),根據(jù) ASCII 表分配字符:
因此最終的字符串就是:ab@cd
(4)填充
那為什么要將 Base64 編碼后的字符串分成 4 個一組進行解碼呢?就是因為填充??=?
??。填充 ??=?
? 對于 base64 編碼是否有必要呢,因為在解碼時又丟棄了填充。主要考慮兩種情況:
- 發(fā)送單個字符的字符串時不需要填充;
- 發(fā)送多個字符的字符串的 base64 編碼時,填充就很重要。如果連接未填充的字符串,則將無法獲得原始字符串,因為有關添加的字節(jié)的信息將丟失。
來看下面的例子:
當發(fā)送連接時沒有填充, 合并的 Base64 字符串將是:YQYmMZGVm,嘗試解碼它時,會得到如下字符串,這是不正確的:
當使用填充發(fā)送連接時, 合并的 Base64 字符串將是:YQ==YmM=ZGVm,嘗試解碼它時,會得到以下字符串,這是正確的:
所以,當傳輸多個字符時,填充是很有必要的。
因為基本上 Base64 在填充的情況下會將 3 個字節(jié)編碼為 4 個 ASCII 字符。四個 ASCII 字符中的每一個都將作為 1 個字節(jié)通過網(wǎng)絡發(fā)送。因此,最終的尺寸總是比原來的尺寸大 33.33%。因此,如果字符串的原始大小為 n 個字節(jié),則經(jīng)過 base64 編碼后的大小將為:
JavaScript 編解碼
最后來看看 JavaScript 中提供的關于編解碼的方法。
(1)UTF-8
URL 只能包含標準的 ASCII 字符,所以必須對其他特殊字符進行編碼。在 JavaScript 中,可以通過了以下方法對 URL 來做 UTF-8 編碼與解碼:
- encodeURI?、decodeURI
- encodeURIComponent?、decodeURIComponent
這里的編碼指的就是將二進制數(shù)據(jù)轉換為 ASCII 格式的方式,解碼反之亦然,即將 ASCII 格式轉換回原始內(nèi)容。
那 encodeURI? 和 encodeURIComponent 有什么區(qū)別呢?
- encodeURI 用于編碼完整的 URL:
- encodeURIComponent 用于編碼 URI 組件,例如查詢字符串:
需要注意,有 11 個字符不能使用 encodeURI? 編碼,而需要使用 encodeURIComponent 進行編碼:
另外,decodeURI 和 decodeURIComponent 是對分別由 encodeURI 和 encodeURIComponent 編碼的字符串進行解碼的方法。
需要注意,encodeURIComponent? 不編碼 - _ . ! ~ * ' ( )。如果要對這些字符進行編碼,則必須將它們替換為相應的 UTF-8 字符序列:
解碼函數(shù)如下所示:
(2)Base64
在JavaScript 中,可以使用 btoa()?(binary to ASCII)和 atob()(ASCII to binary)方法來做 Base64 的編碼和解碼。主要是用于 Data URIs。
下面來對字符串 HelloWorld 做 Base64 的編碼與解碼:
由于ASCII 無法表示中文,因此要先做 UTF-8 編碼,然后再做Base64 編碼;解碼方式為先做 Base64 解碼,再做UTF-8 解碼: