[note] Unicode 字串編碼(encode, decode)
keywords: unicode
, encode
, decode
, escape
, sequence
, 代理編碼
此文章為各筆記之整理,非原創,擷取來源可見參考資料:
- 工具:全字庫:方便查詢 Unicode
- 字符編碼筆記:ASCII,Unicode 和 UTF-8 @ 阮一峰的網絡日誌
- Unicode 與 JavaScript 詳解 @ 阮一峰的網絡日誌
- Javascript Unicode @ Andyyou
重點總結(tl;dr)
- 一個位元組(byte)由 8 個位元(bit)組成,每個位元都是二進制的,共可表徵 255 種狀態。
- 一個位元組可以用 2 個位數的十六進制表示。
- Unicode 的代碼點(Code Point)大多是以
U+
作為前綴,後面放十六進位制,例如,U+0041
即表示文字符號A
。 0x
,0o
前綴的內容,其型態仍然是數值。- 在 JS 中,看到
0x
,0o
的前輟表示為 Unicode 十六進位制和八進位制的碼點,可以透過String.fromCodePoint()
轉換成字符,例如0x0041
。 - 在 JS 中,看到
\x
,\u
,\u{}
的前輟均為U+
碼點的字串跳脫表示法,可以直接透過console.log('')
輸入即會產生文字符號 ,例如\x41
或\u0041
。 - 在 CSS 中,看到
\
的前輟亦表示 Unicode 十六進位制,可以將它改成\u<hex>
在console
輸入即可看到結果,例如\0041
。 - 在 HTML 或 URI 中,看到以
%
作為前綴通常是將特殊字元轉成 UTF-8,可以透過decodeURI()
或decodeURIComponent()
反解回來。 - 在 HTML 中,看到以
&
開頭,以;
結束,表示透過 HTML Encode 編碼過。 - ASCII Code 的十六進位制,會對應到相同的 Unicode 十六進位制,以及相同的 UTF-16 十六進位制。
- 查詢某一文字符號的編碼可到 unicode.scarfboy.com。
- 進制轉換:
// 轉成 10 進制,可以直接輸入不用使用 Number()
0xfe; // 254
Number('0xfe')(
// 254
// 10 進制轉成其他進制
254,
)
.toString(16)(
// "fe",轉成 16 進制
254,
)
.toString(2); // "11111110",轉成 2 進制
其實可以不用加上
Number()
直接輸入0xfe
即可。
- 碼點與文字轉換:
// 根據碼點轉為文字,使用數值
String.fromCodePoint(0x41); // "A"
String.fromCodePoint(65); // "A"
// 根據碼點轉為文字:使用跳脫字符
// \x<兩位碼點>,只使用到 ASCII
// \u<四位碼點>,只使用到 Unicode 基本平面
// \u{碼點},使用到 Unicode 輔助平面
console.log('\x41'); // "A"
console.log('\u0041'); // "A"
console.log('\u{41}'); // "A"
console.log('\u{0041}'); // "A"
// 根據文字轉回碼點
const character = 'A';
character
.codePointAt()(
// 65
65,
)
.toString(16); // 0x41
進位制
進位制的表示法
keywords: bin
, oct
, dec
, hex
進位制(縮寫) | 前綴法 | 首字法 | 下標表示法 | HTML 表示法 |
---|---|---|---|---|
二進位(Binary;bin) | 0b 11111110 | 11111110b | (11111110)2 | |
八進位(Octal;oct) | 0 376, 0o 376 | 376o | (376)8 | |
十進位(Decimal;dec) | 254 | 254d | (254)10 | |
十六進位(Hexadecimal;hex) | 0x fe | feh | (fe)16 | &#x 5a3; |
- 在 HTML ,十六進制可以用
x
表示,例如֣
和֣
相同,5a3
是 16 進制,轉成 10 進制後就是1443
。
在 HTML 中常用
&
開頭;
結尾當作一種編碼方式。
位元(bit)與位元組(byte)
keywords: bit
, byte
- 位元(bit):1 個二進制的值稱作一個位元(bit),全名是 binary digit,有
0
和1
兩種狀態。 - 位元組(byte):8 個二進制位元,組成一個位元組(byte),從
00000000
到11111111
,共可表徵 255 種狀態。在中國位元組稱作字節。
/**
* 1 個位元組(byte)包含 8 個位元的二進制,可以用兩位數的十六進制值表示
**/
0b11111111; // 255
Number('0b11111111'); // 255, 11111111b 等同於 255d
(255).toString(16); // 'ff'
- 一個位元組(byte)由 8 個位元(bit)組成,每個位元都是二進制的,共可表徵 255 種狀態,並且可用 2 位數的十六進制表示。
- 留意位元和位數的差別,位元均是由二進制組成,而位數是指該數值需要使用幾個位數。
使用 JS 進行進位制轉換
New number and Math features in ES6 @ 2ality
轉換成 10 進位
keywords: Number
, parseInt
// 內容需要以前綴法表示
254; // 254,直接帶入就是 10 進制
0376; // 254,以舊八進制的前綴法表示
0o376; // 254,以新的八進制的前綴法表示
0xfe; // 254,以十六進制的的前綴法表示
0b11111110; // 254,以 二進制的的前綴法表示
Number('0b11111110'); // 如果要用字串的話可以加上 Number()
// 指定該內容要使用的進位制
parseInt('fe', 16); // 254, 0xfe = 254
parseInt('376', 8); // 254, 0o376 = 254
parseInt('11111110', 2); // 254, 0b11111110 = 254
其實可以不用加上
Number()
直接輸入0xf4
等等即可。
10 進位轉換成其他進位
keywords: toString
// 10 進位轉成 16 進位
(254).toString(16); // "fe",0xfe = 254
ASCII(美國資訊交換標準碼)
keywords: symbol
, 文字符號
美國制定了一套字符編碼,用來定義英語字母與二進制之間的關係,這被稱為 ASCII 碼。
ASCII 碼一共規定了 128 個字符的編碼,比如空格 SPACE
是 32,也就是 (00100000)2;大寫的字母 A
是 65,也就是 (01000001)2。
這 128 個符號(包括 32 個不能打印出來的控制符號),只佔用了一個位元組的後面 7 位,最前面的一位統一規定為 0
。
在後面的文章把所有語言的單字,稱為文字符號、符號或字符(Symbol),例如英文的
a
;中文的國
; 法文的é
;阿拉伯文的Ain
。
ASCII 碼是美國最早用來定義二進制文字符號(Symbol)間對應關係的表。
Unicode(萬國碼)
keywords: 碼點
, 編碼位置
, code point
什麼是 Unicode(U+<hex>
)
由於 ASCII 只能儲存最多 256 種組合,顯然對於多國語系來說相當不夠,因此出現 Unicode。
Unicode 就像一個資料庫,每一個文字符號(symbol)都可以對應到一組數字。
在 Unicode 中常用 U+<hex>
表示,U+
後面的 16 進制數就叫「碼點(Code Point)」或「編碼位置」。這個編碼位置是唯一的。透過這種方式可以簡單的存取特定文字符號而不用直接輸入符號本身,例如,U+0000
則碼點為 0000
。
此外 ASCII 的 128 個符號可以直接對應到 Unicode。
`A` = `U+0041`
`a` = `U+0061`
`©` = `U+00A9`
`☃` = `U+2603`
`💩` = `U+1F4A9`
具體的符號對應表,可以在 unicode.org 中查詢 Code Charts。
- 每一個文字符號都可以對應到一個十六進制數字,這個數字就稱為
編碼位置/碼點(Code Point)
。- ASCII 可以直接對應到 Unicode。
基本平面和輔助平面(BMP and SMP)
Unicode 編碼可能的範圍從 U+0000
到 U+10FFFF
超過 110 萬個符號。
基本平面(BMP, Basic Multilingual Plane):由兩個位元組組成,是 Unicode 中最前面的 65536 個文字符號,即 (ffff)16 ,它的碼點範圍是從 0 一直到 216 - 1,寫成 16 進制就是從U+0000
到 U+FFFF
。所有最常見的字符都放在這個平面,這是 Unicode 最先定義和公佈的一個平面。
輔助平面(SMP, Supplementary planes or Astral planes):由三個位元組組成,除了基本平面外,剩下的文字符號都放在輔助平面,碼點範圍從 U+010000
一直到 U+10FFFF
,如果某個字符需要超過 4 位數的 16 進制來表示那它就屬於輔助平面。
65536 是由兩個位元組組成,即
Number('0xffff')
Unicode 的問題:為什麼需要 UTF-8 或其他編碼方式
Unicode 解決的問題是將多國語言中的每一個文字符號都能夠對應到一組數字,但 Unicode 並沒有說明儲存這些數字的方式。例如「臺」的 Unicode 是 U+81FA
,換成二進制的話將是 1000000111111010
,一共需要 16 個位元,也就是 2 個位元組。對於其他的符號則可能需要 3 個以上的位元組。
由於在 ASCII 中用 1 個位元組來表徵符號,因此當納入 Unicode 的時候,需要:
- 當輸入一個以上的位元組時,電腦如何判斷它是使用 ASCII 或 Unicode?電腦如何知道是 1 個位元組來表示一個文字符號,或者 2 個、3 個位元組才表示一個 文字符號?
- 如果統一規定用 3 個位元組來當作一個文字符號的話,那麼對於只需要 1 個位元組的英文來說,將會有 2 個位元組都是 0 ,對於空間的存儲來說造成很大的浪費。
為了解決這樣的問題,需要一種統一的編碼方式,便產生了網路上最廣泛使用的 UTF-8。
Unicode 解決的問題是將所有的文字符號都能對應到一組數字,但沒有規範該如何儲存和使用這組數字。
Unicode 的實作方式:UTF-8, UTF-16, UTF-32
UTF-8
UTF-8 是在網際網路上最廣泛被採用的一種 Unicode 的實現方式。
另外仍有 UTF-16(字母用 2 個 或 4 個位元組表示)和 UTF-32(字母用 4 個位元組表示),但在網際網路上基本上不被使用。
UTF-8 只是 Unicode 的實現方式之一。
UTF-8 是變動位元組長度的一種編碼方式,不同的文字符號可能由 1 個位元組到 4 個位元組來表示,根據不同的文字符號而變化位元組的長度。
UTF-8 的編碼規則很簡單,只有二條:
- 對於單一位元組的文字符號,位元組的第一位設為
0
,後面 7 位為這個符號的 Unicode 碼。因此對於英語字母,UTF-8 編碼和 ASCII 碼是相同的。 - 對於
n
位元組的文字符號(n > 1
),第一個位元組的前n
位都設為1
,第n + 1
位設為0
,後面位元組的前兩位一律設為10
。剩下的沒有提及的二進制位,全部為這個符號的 Unicode 碼。
範例一:A
舉例來說,英文字「A」的 Unicode 是 U+0041
,也就是 (65)10,亦等同於 (0100 0001)2 因此根據上表顯示介於十六進制 0000 0000
~ 0000 007F
範圍內,也就是屬於第一組。
所以「A」的 UTF-8 編碼只需要一個位元組,格式就是 0xxxxxxx
。將「A」Unicode 的內容直接填入 x,即可得到「A」的 UTF-8 編碼為 0100 0001
,轉換成 16 進制就是 (41)16。
const character = 'A';
character
.codePointAt()(
// 65
65,
)
.toString(2)(
// 0b1000001
65,
)
.toString(16); // 0x41
範例二:臺
舉例來說,中文字「臺」的 Unicode 是 U+81FA
,也就是 (33274)10,亦等同於 (1000 0001 1111 1010)2,因此根據上表顯示介於十六進制 00000800
~ 0000FFFF
這個範圍內,也就是屬於第三組。
所以「臺」的 UTF-8 編碼需要 三個位元組,格式就是 1110xxxx 10xxxxxx 10xxxxxx
。然後從「臺」的最後一個二進制位數開始,依次由後往前添入格式中的 x
,多出來的位數補 0
。
於是便可得到「臺」的 UTF-8 編碼是 11101000 10000111 10111010
,轉換成十六進制就是 (E887BA
)16。
臺 =
U+81FA
= UTF-8 Hex(E887BA
) = UTF-16 Hex(81FA
) = URI-encoded UTF-8%E8%87%BA
= CSS\81fa
= JS\u81fa
const character = '臺';
encodeURIComponent(character); // '%E8%87%BA'
character
.codePointAt()(
// Dec(33274)
33274,
)
.toString(16); // Hex(81FA)
String.fromCodePoint(33274); // '臺'
console.log('\u81fa'); // '臺'
Number(0b11101000).toString(16); // 'e8',將 0b11101000 轉成 16 進制
Number(0b10000111).toString(16); // '87'
Number(0b10111010).toString(16); // 'ba'
範例三:严
簡體字「严」的 Unicode 是 U+4E25
,也就是 (100 111000 100101
)2,因此根據上表是介於十六進制 00000800
~ 0000FFFF
這個範圍內,也就是屬於第三組。
因此「严」的 UTF-8 編碼需要三個位元組,即格式是1110xxxx 10xxxxxx 10xxxxxx
。然後,從「严」的最後一個二進制位數開始,依次從後向前填入格式中的x
,多出的位補0
。
這樣就得到了,严
的 UTF-8 編碼是11100100 10111000 10100101
,轉換成十六進制就是E4B8A5
。
綜合上述,严
的 Unicode 碼是U+4E25
,UTF-8 編碼是 (E4B8A5
)16。
严 =
U+4E25
= UTF-8 Hex(E4B8A5
) = UTF-16 Hex(4e25
) = URI-encoded UTF-8%E4%B8%A5
= CSS\4E25
= JS\u4E25
const character = '严';
encodeURIComponent(character); // '%E4%B8%A5'
character
.codePointAt()(
// 20005
20005,
)
.toString(16); // 0x4E25
String.fromCodePoint(20005); // '严'
console.log('\u4E25'); // '严'
UTF-32
相較於 UTF-8,UTF-32 是 Unicode 實作是最簡單的方式,每個碼點(Code Point)都使用 4 個位元組表示(也就是 8 位數的 16 進制),位元組內容一一對應碼點,不足的補 0 。
缺點是浪費過多空間(很多位元組都是 0),導致沒人使用,HTML 5 明文規定不可將網頁編碼為 UTF-32。
U+0000 = 0x00000000
U+597D = 0x0000597D
每 1 個位元組,可以用 2 位數十六進制表示。
UTF-16
UTF-16 編碼介於 UTF-32 與 UTF-8 之間,同時結合了固定位元組長度和變動位元組長度這兩種編碼方法的特點。
UTF-16 的編碼規則很簡單,基本平面(BMP)用 2 個位元組(U+0000
~ U+FFFF
)表示;輔助平面(SMP)用 4 個位元組(U+01000
~ U+10FFFF
)表示。
- 每 1 個位元組可以由 2 位數的十六進制表示。
- UTF-16 和 Unicode 十六進位制(
U+
)是不同的
Unicode 基本平面的文字符號
對於 Unicode 基本平面的文字符號轉成 UTF-16 時,直接將碼點轉為對應的十六進制形式,長度為 2 個位元組即可,因次介於 U+0000
到 U+FFFF
。例如:
U+597D = UTF-16(0x597D)
對於 Unicode 基本平面的文字符號來說,Unicode 的值會直接等同於 UTF-16 的值。
Unicode 輔助平面的文字符號
由於 Unicode 輔助平面的文字符號超過兩個位元組,因此需要判斷這兩個位元組到底是屬於一個文字符號,還是兩個文字符號。
由於 Unicode 基本符號從 U+D800
到 U+DFFF
是一個空段,因此在 UTF-16 中,把一個輔助平面的文字符號,可以拆成兩個基本平面中空段的文字符號來表示,也就是把輔助平面映射到基本平面上。其中一半映射到 0xD800
~ 0xDBFF
;另一半則映射到 0xDC00
~ 0xDEFF
上。
輔助平面的字符(U+010000
到 U+10FFFF
)在 UTF-16 中佔用 4 個位元組:
// U+1D306 轉換為 UTF-16 後為 0xD834 0xDF06
H = Math.floor((0x1d306 - 0x10000) / 0x400) + 0xd800 = 0xd834;
L = ((0x1d306 - 0x10000) % 0x400) + 0xdc00 = 0xdf06;
在 JavaScript 中使用 Unicode
JavaScript 最早用的是 UCS-2
,因為當時仍未出現 UTF-16 的編碼。這導致所有超過兩個位元組的文字符號在編碼時可能出現錯誤,例如 4 個位元組表示的文字符號,會被拆成 2 個雙位元組的文字符號處理。
後來 UTF-16
取代了 UCS-2 ,或者說 UCS-2 整合進了 UTF-16。所以現在只有 UTF-16,沒有 UCS-2。因此在目前 JavaScript 中,主要是支援 UTF-16,但在操作時顯示的都是 Unicode 的編碼位置。
對於 Unicode 輔助平面的文字符號,使用傳統的 JavaScript 函式可能會產生錯誤的結果。