每個(gè)文本編輯器都有默認(rèn)的編碼方式(比如 UTF-8 編碼),當(dāng)我們保存文檔的時(shí)候,可以選擇編碼方式,如果沒有特意選擇編碼方式,文本編輯器就會(huì)以默認(rèn)的編碼方式將文本內(nèi)容轉(zhuǎn)換為二進(jìn)制,存儲(chǔ)到計(jì)算機(jī)的硬盤里。我們打開文本編輯器,新建一篇文檔,只輸入兩個(gè)漢字 你好,然后以 UTF-8 的編碼方式保存文檔,那么硬盤里存儲(chǔ)的是二進(jìn)制的數(shù)據(jù) 11100100 10111101 10100000 11100101 10100101 10111101。以上這種,將文本字符按照某種規(guī)則轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)的過程,就稱之為編碼。反過來,將二進(jìn)制數(shù)據(jù)按照某種規(guī)則轉(zhuǎn)換為文本字符的過程,就稱之為解碼,比如用文本編輯器打開一篇已經(jīng)存在的文檔,文本編輯器會(huì)根據(jù)文檔的編碼方式,將硬盤里的二進(jìn)制數(shù)據(jù)轉(zhuǎn)換為文本字符顯示出來。
字符集和字符編碼
我們先來看幾個(gè)概念:
- 編碼:按照某種規(guī)則,將字符轉(zhuǎn)換為二進(jìn)制數(shù)存儲(chǔ)在計(jì)算機(jī)中
- 解碼:按照某種規(guī)則,將計(jì)算機(jī)中存儲(chǔ)的二進(jìn)制數(shù)轉(zhuǎn)換為字符,顯示出來
- 字符集:文字符號(hào)的集合,比如 ASCII 字符集、GB2312 字符集、Unicode 字符集
- 字符編碼:是一套法則,將字符集轉(zhuǎn)換為二進(jìn)制數(shù)
ASCII 字符集和字符編碼
ASCII(American Standard Code for Information Interchange,美國(guó)信息交換標(biāo)準(zhǔn)代碼)是基于拉丁字母的一套電腦編碼系統(tǒng)。它主要用于顯示現(xiàn)代英語,而其擴(kuò)展版本 EASCII 則可以勉強(qiáng)顯示其他西歐語言。它是現(xiàn)今最通用的單字節(jié)編碼系統(tǒng)(但是有被 Unicode 追上的跡象),并等同于國(guó)際標(biāo)準(zhǔn)ISO/IEC 646。
- ASCII 字符集:一共包括 128 個(gè)字符,主要包括控制字符(回車鍵、退格、換行鍵等),可顯示字符(英文大小寫字符、阿拉伯?dāng)?shù)字和西文符號(hào))。
- ASCII 編碼:將 ASCII 字符集轉(zhuǎn)換為二進(jìn)制數(shù)的規(guī)則,使用 7 位(bits)表示一個(gè)字符。
- EASCII 字符集:一共包括 256 個(gè)字符,在 ASCII 字符集的基礎(chǔ)上,擴(kuò)展了一些歐洲常用字符。
- EASCII 編碼:將 EASCII 字符集轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)的規(guī)則,使用 8 位(bits)也就是一個(gè)字節(jié)表示一個(gè)字符。
ASCII 字符集映射到數(shù)字編碼規(guī)則如下圖所示:

GB2312 字符集和字符編碼
計(jì)算機(jī)發(fā)明之處及后面很長(zhǎng)一段時(shí)間,只用應(yīng)用于美國(guó)及西方一些國(guó)家,ASCII 能夠很好滿足用戶的需求。但是當(dāng)中國(guó)也有了計(jì)算機(jī)之后,為了顯示中文,必須設(shè)計(jì)一套編碼規(guī)則用于將漢字轉(zhuǎn)換為計(jì)算機(jī)可以接受的數(shù)字系統(tǒng)的數(shù)。于是在 ASCII 字符集的基礎(chǔ)上,擴(kuò)展出了 GB2312 字符集。GB2312 字符集規(guī)定:一個(gè)小于 127 的字符的意義與原來相同,但兩個(gè)大于 127 的字符連在一起時(shí),就表示一個(gè)漢字,前面的一個(gè)字節(jié)(稱之為高字節(jié))從 0xA1 到 0xF7,后面一個(gè)字節(jié)(低字節(jié))從 0xA1 到 0xFE,這樣我們就可以組合出大約 7000 多個(gè)簡(jiǎn)體漢字了。在這些編碼里,還把數(shù)學(xué)符號(hào)、羅馬希臘的 字母、日文的假名們都編進(jìn)去了,連在 ASCII 里本來就有的數(shù)字、標(biāo)點(diǎn)、字母都統(tǒng)統(tǒng)重新編了兩個(gè)字節(jié)長(zhǎng)的編碼,這就是常說的 "全角" 字符,而原來在 127 號(hào)以下的那些就叫 "半角" 字符了。
- GB2312 字符集:在 ASCII 字符集的基礎(chǔ)上,擴(kuò)展了 7000 多個(gè)簡(jiǎn)體漢字以及標(biāo)點(diǎn)符號(hào)等字符。
- GB2312 字符編碼:將 GB2312 字符集轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)的規(guī)則,屬于 ASCII 字符集的字符占一個(gè)字節(jié),中文(包括全角標(biāo)點(diǎn)符號(hào))占兩個(gè)字節(jié)。
Unicode 字符集和字符編碼
Unicode 是為了解決傳統(tǒng)的字符編碼方案的局限而產(chǎn)生的,它為每種語言中的每個(gè)字符設(shè)定了統(tǒng)一并且唯一的二進(jìn)制編碼,以滿足跨語言、跨平臺(tái)進(jìn)行文本轉(zhuǎn)換、處理的要求。Unicode 字符集中每個(gè)字符對(duì)應(yīng)的二進(jìn)制數(shù),又稱之為 Unicode 碼點(diǎn),如果用十進(jìn)制來表示碼點(diǎn),常用字符的碼點(diǎn)在 0-65535 范圍之內(nèi),碼點(diǎn)超過 65535 的字符叫做輔助平面(星芒層)。Unicode 是字符集,UTF-32 / UTF-16 / UTF-8 是三種字符編碼方案。
- Unicode 字符集:包含世界上所有語言的文字和符號(hào)。
- Unicode 碼點(diǎn):每個(gè)字符對(duì)應(yīng)一個(gè) Unicode 編碼號(hào),常用的在 0-65535,編碼號(hào)超過 65535 的字符叫做輔助平面(星芒層)。
- UTF-32 編碼:每個(gè)字符都用四個(gè)字節(jié)來存儲(chǔ)。
- UTF-16 編碼:0-65535 的字符用兩個(gè)字節(jié)存儲(chǔ),輔助平面的用四個(gè)字節(jié)存儲(chǔ)。
- UTF-8 編碼:屬于 ASCII 字符集的字符用一個(gè)字節(jié)存儲(chǔ),帶有附加符號(hào)的拉丁文、希臘文、西里爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及它拿字母需要兩個(gè)字節(jié),漢字需要三個(gè)字節(jié),輔助平面四個(gè)字節(jié)。
JavaScript 字符集
JavaScript 使用 Unicode 字符集,使用 UTF-16 編碼方式。var foo = '你好' 這段代碼為變量 foo 賦值了一個(gè)字符串,JavaScript 引擎運(yùn)行時(shí)會(huì)在內(nèi)存中分配一部分區(qū)域來保存該字符串。由于內(nèi)存中存儲(chǔ)的都是二進(jìn)制數(shù)據(jù),所以需要將字符串 你好 按照 UTF-16 編碼方式轉(zhuǎn)換為二進(jìn)制,所以內(nèi)存中保存的是 01001111 01100000 01011001 01111101。注意 UTF-16 只是 JavaScript 引擎的編碼方式,而不是 JavaScript 文檔保存時(shí)的編碼方式。如果通過文本編輯器將這段 JavaScript 代碼所在的文檔以 UTF-8 的編碼方式保存在硬盤里,那么文檔中 你好 這兩個(gè)字符會(huì)按照 UTF-8 編碼方式轉(zhuǎn)換為二進(jìn)制 11100100 10111101 10100000 11100101 10100101 10111101。也就是說硬盤里保存的是 UTF-8 編碼的二進(jìn)制數(shù)據(jù),而內(nèi)存里保存的是 UTF-16 編碼的二進(jìn)制數(shù)據(jù)。
URL 的編碼和解碼
網(wǎng)頁(yè)的 URL 只能包含合法的字符,這可以分成兩類。
URL 元字符:分號(hào)(;),逗號(hào)(,),斜杠(/),問號(hào)(?),冒號(hào)(:),at(@),&,等號(hào)(=),加號(hào)(+),美元符號(hào)($),井號(hào)(#)
語義字符:a-z,A-Z,0-9,連詞號(hào)(-),下劃線(_),點(diǎn)(.),感嘆號(hào)(!),波浪線(~),星號(hào)(*),單引號(hào)('),圓括號(hào)(())
除了以上字符,其他字符出現(xiàn)在 URL 之中都必須轉(zhuǎn)義,規(guī)則是根據(jù)操作系統(tǒng)的默認(rèn)編碼,將每個(gè)字節(jié)轉(zhuǎn)為百分號(hào)(%)加上兩個(gè)大寫的十六進(jìn)制字母。比如,UTF-8 的操作系統(tǒng)上,http://www.example.com/q=春節(jié) 這個(gè) URL 之中,漢字“春節(jié)”不是 URL 的合法字符,所以被瀏覽器自動(dòng)轉(zhuǎn)成 http://www.example.com/q=%E6%98%A5%E8%8A%82。其中,“春”轉(zhuǎn)成了%E6%98%A5,“節(jié)”轉(zhuǎn)成了%E8%8A%82。這是因?yàn)椤按骸焙汀惫?jié)“的 UTF-8 編碼分別是E6 98 A5和E8 8A 82,將每個(gè)字節(jié)前面加上百分號(hào),就構(gòu)成了 URL 編碼。
JavaScript 提供四個(gè) URL 的編碼/解碼方法。
- encodeURI()
- encodeURIComponent()
- decodeURI()
- decodeURIComponent()
encodeURI() 會(huì)將元字符和語義字符之外的字符,都進(jìn)行轉(zhuǎn)義,用于轉(zhuǎn)碼整個(gè) URL。
encodeURIComponent() 轉(zhuǎn)碼除了語義字符之外的所有字符,即元字符也會(huì)被轉(zhuǎn)碼。所以,它不能用于轉(zhuǎn)碼整個(gè) URL,常用于轉(zhuǎn)碼 URL 片段。
decodeURI() 是 encodeURI() 方法的逆操作。
decodeURIComponent() 是 encodeURIComponent() 方法的逆操作。
文本格式與二進(jìn)制格式
文件一般分為文本文件和二進(jìn)制文件。在計(jì)算機(jī)中,數(shù)據(jù)都是以二進(jìn)制的方式存儲(chǔ)的,所以不管是文本文件還是二進(jìn)制文件,在計(jì)算機(jī)的內(nèi)存或硬盤里,都是以二進(jìn)制的方式保存的。當(dāng)我們讀取文本文件時(shí),需要按照文件的字符編碼方式 (比如 UTF-8) 來解碼,將計(jì)算機(jī)中存儲(chǔ)的二進(jìn)制數(shù)據(jù)轉(zhuǎn)換為文本字符,在客戶端顯示出來。當(dāng)我們讀取二進(jìn)制文件時(shí),需要根據(jù)不同類型二進(jìn)制文件的編碼方式 (比如 JPEG MP3) 來解碼,將計(jì)算機(jī)中存儲(chǔ)的二進(jìn)制數(shù)據(jù)轉(zhuǎn)換為圖片視頻等形式,在客戶端顯示出來。
網(wǎng)絡(luò)協(xié)議也可以分為文本協(xié)議和二進(jìn)制協(xié)議。在網(wǎng)絡(luò)傳輸中,數(shù)據(jù)都是以二進(jìn)制的方式流動(dòng)的,所以網(wǎng)絡(luò)設(shè)備接收到的數(shù)據(jù)包都是二進(jìn)制的。當(dāng)通過文本協(xié)議接收到數(shù)據(jù)包后,首先需要根據(jù)文本協(xié)議的編碼方式 (比如 UTF-8),將數(shù)據(jù)包中的二進(jìn)制數(shù)據(jù)進(jìn)行解碼,轉(zhuǎn)換為文本字符,然后再進(jìn)行相關(guān)操作。比如 HTTP 協(xié)議就是文本協(xié)議,當(dāng)客戶端接收到服務(wù)器響應(yīng)的一段數(shù)據(jù)之后,首先將二進(jìn)制數(shù)據(jù)包轉(zhuǎn)換為文本字符,假設(shè)文本字符中有這么一段內(nèi)容 Content-type: text/html,根據(jù) HTTP 協(xié)議的規(guī)定,這是說服務(wù)器響應(yīng)的數(shù)據(jù)類型是 HTML 文檔,那么客戶端便可以將響應(yīng)的數(shù)據(jù)按照 HTML 文檔格式來解析。可見,如果網(wǎng)絡(luò)協(xié)議是文本協(xié)議,數(shù)據(jù)接收方必須先將二進(jìn)制數(shù)據(jù)轉(zhuǎn)換為文本數(shù)據(jù)之后才能得到信息,也就是說所有的信息都是以文本的方式來傳達(dá)的。
如果網(wǎng)絡(luò)協(xié)議是二進(jìn)制的協(xié)議,那么數(shù)據(jù)接收方直接根據(jù)二進(jìn)制數(shù)據(jù)便可以獲得信息,不需要將二進(jìn)制數(shù)據(jù)轉(zhuǎn)換為文本數(shù)據(jù)。假設(shè)根據(jù)某種二進(jìn)制協(xié)議,數(shù)據(jù)中的第一個(gè)字節(jié)代表這段數(shù)據(jù)的類型,比如 1 代表 TEXT 文本,2 代表 HTML 文檔,3 代表 JPEG 圖片 ... 當(dāng)客戶端接收到了一段如下的數(shù)據(jù):00000010 10010000 00100101 ...,那么根據(jù)第一個(gè)字節(jié) 00000010,便知道這段數(shù)據(jù)是一篇 HTML 文檔,然后客戶端會(huì)按照 HTML 文檔的方式來解析。這就相當(dāng)于文本協(xié)議中的 Content-type: text/html。
由此可見,網(wǎng)絡(luò)傳輸時(shí)候,二進(jìn)制協(xié)議的數(shù)據(jù)所占的體積更小,而且不需要先將二進(jìn)制數(shù)據(jù)轉(zhuǎn)換為文本格式,因此效率更高。
編碼轉(zhuǎn)換方式
ASCII 字符集中,有一部分是不可見字符,比如 ASCII 碼位在 0-31 的字符都是不可見的。的在網(wǎng)絡(luò)上交換數(shù)據(jù)時(shí),比如說從 A 地傳到 B 地,往往要經(jīng)過多個(gè)路由設(shè)備,不同的設(shè)備對(duì)字符的處理方式有一些不同,那些不可見字符就有可能被處理錯(cuò)誤,這是不利于傳輸?shù)?。另外一些網(wǎng)絡(luò)協(xié)議(比如電子郵件)或者網(wǎng)絡(luò)設(shè)備不能處理非 ASCII 字符集中的字符,因此我們?cè)趥鬏敂?shù)據(jù)前,可以按照某種編碼方式,先將數(shù)據(jù)中的所有字符都轉(zhuǎn)換為 ASCII 字符集中的可見字符,然后再進(jìn)行傳輸。常見的編碼轉(zhuǎn)換方式有 Quoted-printable 和 Base64。
Quoted-printable 編碼
簡(jiǎn)單來說,Quoted-printable 編碼就是將每一個(gè) 8 位的字節(jié),轉(zhuǎn)換為三個(gè)字符。第一個(gè)字符是 "=" 號(hào),這是固定不變的。后面二個(gè)字符是二個(gè)十六進(jìn)制數(shù),分別代表了這個(gè)字節(jié)前四位和后四位的數(shù)值。如果某個(gè) 8 位的字節(jié)是可打印的 ASCII 碼字符(十進(jìn)制值從 33 到 126),那么該字節(jié)保持原樣不變,"="(十進(jìn)制值61)除外。下面詳細(xì)介紹 Quoted-printable 編碼的轉(zhuǎn)換規(guī)則。
數(shù)據(jù)在計(jì)算機(jī)中以二進(jìn)制存儲(chǔ),Quoted-printable 編碼以 8 位的字節(jié)為單位轉(zhuǎn)換數(shù)據(jù)。如果某個(gè) 8 位的字節(jié)是可打印的 ASCII 碼字符(十進(jìn)制值從 33 到 126),那么該字節(jié)保持原樣不變, "="(十進(jìn)制值 61)除外。如果某個(gè) 8 位的字節(jié)是不可打印字符 (十進(jìn)制值 0-31 127),或者是 "=" 號(hào) (十進(jìn)制值 61),則該字節(jié)需要轉(zhuǎn)換。具體轉(zhuǎn)換規(guī)則如下:將該字節(jié)轉(zhuǎn)換為三個(gè)字節(jié),第一個(gè)字節(jié)固定為 "=" 號(hào)的 ASCII 編碼 00111101;第二個(gè)字節(jié)取原來字節(jié)的前 4 個(gè) bit 位,然后前面補(bǔ)上 4 個(gè) 0;第三個(gè)字節(jié)取原來字節(jié)的后 4 個(gè) bit 位,然后前面補(bǔ)上 4 個(gè) 0。
舉例來說,字符串 A嚴(yán)B 按照 UTF-8 編碼轉(zhuǎn)換為二進(jìn)制 01000001 11100100 10111000 10100101 01000010 存儲(chǔ)在計(jì)算機(jī)中。我們對(duì)該數(shù)據(jù)進(jìn)行 Quoted-printable 編碼,第一個(gè)字節(jié) 01000001 是可打印的 ASCII 字符,所以不需要改變。第二個(gè)字節(jié) 11100100 是不可打印的 ASCII 字符,需要轉(zhuǎn)換為 00111101 00001110 00000100 (對(duì)應(yīng)的 ASCII 字符為 =E4),其中 00111101 是字符 "=" 的 ASCII 編碼,而 00001110 和 00000100 是將原來的字節(jié) 11100100 拆開來,通過對(duì)前 4 位和后 4 位高位補(bǔ) 0,各形成了一個(gè)新的字節(jié)。后面的字節(jié)同理。因此 01000001 11100100 10111000 10100101 01000010 經(jīng)過 Quoted-printable 編碼成為 00111101 00111101 00001110 00000100 00111101 00001011 00001000 00111101 00001010 00000101 01000010 (對(duì)應(yīng)的 ASCII 字符為 A=E4=B8=A5B)。這樣,我們就將 UTF-8 字符串 A嚴(yán)B 轉(zhuǎn)換成了 ASCII 字符串 A=E4=B8=A5B,轉(zhuǎn)換后的字符串中的所有字符都是 ASCII 字符集中的可打印字符。
再舉一個(gè)例子,UTF-8 字符串 a=你好 會(huì)轉(zhuǎn)換為 ASCII 字符串 a=3D=E4=BD=A0=E5=A5=BD,字符 "a" 保持不變,字符 "=" 轉(zhuǎn)換為 "=3D",字符 "你" 轉(zhuǎn)換為 "=E4=BD=A0",字符 "好" 轉(zhuǎn)換為 "=E5=A5=BD"。
Base64 編碼
所謂 Base64 編碼,就是說選出 64 個(gè)字符 ---- 小寫字母a-z、大寫字母A-Z、數(shù)字0-9、符號(hào)"+"、"/"(再加上作為墊字的"=",實(shí)際上是65個(gè)字符) ---- 作為一個(gè)基本字符集。然后,其他所有符號(hào)都轉(zhuǎn)換成這個(gè)字符集中的字符。具體來說,轉(zhuǎn)換方式可以分為四步。
- 將每三個(gè)字節(jié)作為一組,一共是24個(gè)二進(jìn)制位。
- 將這24個(gè)二進(jìn)制位分為四組,每個(gè)組有6個(gè)二進(jìn)制位。
- 在每組前面加兩個(gè)00,擴(kuò)展成32個(gè)二進(jìn)制位,即四個(gè)字節(jié)。
- 根據(jù)下表,得到擴(kuò)展后的每個(gè)字節(jié)的對(duì)應(yīng)符號(hào),這就是Base64的編碼值。

舉一個(gè)具體的實(shí)例,演示英語單詞Man如何轉(zhuǎn)成Base64編碼。
- "M"、"a"、"n"的ASCII值分別是77、97、110,對(duì)應(yīng)的二進(jìn)制值是01001101、01100001、01101110,將它們連成一個(gè)24位的二進(jìn)制字符串010011010110000101101110。
- 將這個(gè)24位的二進(jìn)制字符串分成4組,每組6個(gè)二進(jìn)制位:010011、010110、000101、101110。
- 在每組前面加兩個(gè)00,擴(kuò)展成32個(gè)二進(jìn)制位,即四個(gè)字節(jié):00010011、00010110、00000101、00101110。它們的十進(jìn)制值分別是19、22、5、46。
- 根據(jù)上表,得到每個(gè)值對(duì)應(yīng)Base64編碼,即T、W、F、u。
如果字節(jié)數(shù)不足三,則這樣處理:
-
二個(gè)字節(jié)的情況:將這二個(gè)字節(jié)的一共16個(gè)二進(jìn)制位,按照上面的規(guī)則,轉(zhuǎn)成三組,最后一組除了前面加兩個(gè)0以外,后面也要加兩個(gè)0。這樣得到一個(gè)三位 的Base64編碼,再在末尾補(bǔ)上一個(gè)"="號(hào)。
比如,"Ma"這個(gè)字符串是兩個(gè)字節(jié),可以轉(zhuǎn)化成三組00010011、00010110、00010000以后,對(duì)應(yīng)Base64值分別為T、W、E,再補(bǔ)上一個(gè)"="號(hào),因此"Ma"的Base64編碼就是TWE=。
-
一個(gè)字節(jié)的情況:將這一個(gè)字節(jié)的8個(gè)二進(jìn)制位,按照上面的規(guī)則轉(zhuǎn)成二組,最后一組除了前面加二個(gè)0以外,后面再加4個(gè)0。這樣得到一個(gè)二位的Base64編碼,再在末尾補(bǔ)上兩個(gè)"="號(hào)。
比如,"M"這個(gè)字母是一個(gè)字節(jié),可以轉(zhuǎn)化為二組00010011、00010000,對(duì)應(yīng)的Base64值分別為T、Q,再補(bǔ)上二個(gè)"="號(hào),因此"M"的Base64編碼就是TQ==。
用 Javascript 語言進(jìn)行 Base64 編碼
在 JavaScript 中,有2個(gè)函數(shù)分別用來處理解碼和編碼base64 字符串:
btoa() 用于將 ASCII 字符串或二進(jìn)制數(shù)據(jù)編碼為 Base64 字符串,atob() 用于將 Base64 字符串解碼為 ASCII 字符串或二進(jìn)制數(shù)據(jù),這兩個(gè)方法只能用于轉(zhuǎn)碼 ASCII 字符集中的字符,對(duì)于不屬于 ASCII 字符集中的字符,使用這兩個(gè)方法會(huì)報(bào)錯(cuò)。
由于 JavaScript 內(nèi)部的字符串都以 UTF-16 的形式進(jìn)行保存的,所以如果有字符超出了 8 位 ASCII 編碼的字符范圍時(shí),在大多數(shù)的瀏覽器中對(duì)Unicode字符串調(diào)用 window.btoa 將會(huì)造成一個(gè) Character Out Of Range 的異常。有 兩種方法 解決這個(gè)問題:
- 第一種方法:先用
encodeURIComponent()轉(zhuǎn)義整個(gè)字符串,然后再編碼。 - 第二種方法:先將 UTF-16 的 DOMString 轉(zhuǎn)碼為 UTF-8 的字符數(shù)組,然后再編碼。
參考
字符集和字符編碼(Charset & Encoding)
Base64筆記
JavaScript 標(biāo)準(zhǔn)參考教程 / 字符串
JavaScript 標(biāo)準(zhǔn)參考教程 / window 對(duì)象
Base64的編碼與解碼