[web] 瞭解網頁中看不懂的編碼:Unicode 在 JavaScript 中的使用
keywords: unicode
, utf-8
, JavaScript
如果只是想要簡單知道 Unicode 是什麼,沒有要瞭解使用方式的話,可以觀看計算機科學速成課(第四集):二進制 ,在此教學影片的後半段有提到文字符號的編碼概念。
在學習網頁開發的過程中,一定會慢慢的碰到所謂的 Unicode
, UTF-8
還有其他幾種不同的編碼方式,這麼說你可能不會太有感覺,讓我們以 Facebook 為例,你可以看到在資料傳遞的過程中,常常有這種看不太懂的東西...
我們把它拿出來編排一下大概長這樣:
"profiles": {
"768320183253420": {
"name": "PJCHENder\u7db2\u9801\u524d\u7aef\u8cc7\u6e90\u7ad9",
"firstName": "PJCHENder\u7db2\u9801\u524d\u7aef\u8cc7\u6e90\u7ad9",
"gender": 11,
"uri": "https:\/\/www.facebook.com\/pjchender\/",
"type": "page",
"businessID": 0,
"businessName": null,
"allBusinessData": [
{
"businessID": 0,
"businessName": null
}]
},
// ...
}
你可以看到裡面有 \u7db2\u9801\u524d\u7aef\u8cc7\u6e90\u7ad9
這種看不太懂的內容。
再舉一個例子來看,有些時候我們可能看到某些網路文章想要分享給別人,明明網址是:https://today.line.me/tw/pc/article/冰火之國+冰島-ownNlj
,可是一分享出去卻變成一堆看似亂碼的東西 https://today.line.me/tw/pc/article/%E5%86%B0%E7%81%AB%E4%B9%8B%E5%9C%8B+%E5%86%B0%E5%B3%B6-ownNlj
:
上面的這些例子都是文字轉成編碼後的情況,你應該可以瞭解到這些編碼在瞭解網頁開發時的重要性的,如果不懂的話,就無法把這些內容解碼回去,那就會看不懂別人的內容阿...
看到編碼卻不知道怎麼解碼,那該怎麼上車呢?
在這篇文章中會說明在網頁中常用的一些編碼方式,讓對網頁編碼沒有概念的捧油們可以對它有些概念,重點是對它不再畏懼,然後有興趣的話可以在透過延伸閱讀中的文章進一步瞭解更多細節。
什麼是字串編碼(String Encode)?
先來談談什麼是字串編碼,字串編碼簡單來說,就是把我們熟悉的文字用許多數字來代表它,你可以想像就像在當兵或坐牢的時候,大家不會直接叫你的名字,而是叫你的編號。
例如,「阿童」在坐牢時的編號是「9453」,這時候只要廣播「9453」請到司令台集合,阿童就會知道這是在叫他,就會跑到司令台來了。
這個編號是不會重複的,也就是「9453」就只會是「阿童」的編號,不會同時有其他淡水阿婆、基隆阿公的編號也一樣是 9543。
能夠代表你的身份證字號也是一種編碼的概念,這個編號就可以用來代表它指稱的是你。
電腦只認得數字
接著你可能會好奇,在監獄裡面因為方便管理、去個人化等因素,所以把人名改成編碼似乎合理,但為什麼需要把我們常用的文字也都變成編碼呢?
這是因為電腦在運算的過程中,底層都還是數字來表示的,電腦只看得懂數字,但並不認得文字符號。也就是它只看的懂「9453」這個數字,但並不認得「阿童」這些字。
更精確的說,電腦只認得「二進制」這種用 0 和 1 組成的數字。
由於電腦只認得數字,但是我們人類看得懂、使用的卻是文字,那麼該怎麼辦呢?於是,美國最早定義了一套文字符號的編碼,能夠將每個英文的文字符號(Symbol)都對應到一個數字,這個最早統一的規定就稱為「ASCII 編碼」。
**文字符號(Symbol)**指的是可以把文字拆成的最小單位,像是英文的「字母」
a
,b
,c
都是文字符號;而中文的「國字」,像是人
,國
,正
這些也都是文字符號。
ASCII 編碼(ASCII Code)
ASCII Code 是由美國制訂,最早用來統一英文文字符號和數字間的對應關係,除了 52 個大小寫英文字母外,其中也包含了常用的符號(如 !
, <
, =
)和特殊字元,總共定義了 127 個關係。
舉例來說,從下面的 ASCII Table 中可以看到,最上面的地方寫了 Dec 和 Char,其中 Char
就是文字符號的部分,Dec
則是用來表示該文字符號的十進位數字,在這張表中可以看到大寫英文字母的 A
對應到的編號竟是十進位的 65
號,小寫英文字母 a
則是對應到十進位的 97
號:
完整的 ASCII Table 可以點這裡。
那麼 ASCII 的編碼方式就是我們最前面提到的, Facebook 中回傳的 \u9673\u67cf\u878d
內 容嗎?答案是:「有關但不同」。
Unicode(萬國碼)
在最前面所提到 Facebook 中回傳的 \u9673\u67cf\u878d
其實用的是 Unicode 編碼?Unicode 編碼又是什麼呢?
你可以發現在 ASCII 中定義了 127 個文字符號和數字之間的關係,但是那是因為英文只需要有 26 個英文字母就可以組成各是各樣的單字,但是中文或者說許多國家的語言並不是這樣阿,很明顯的 ASCII Code 不適用於所有的語言。
此外,如果不同國家使用不同的編碼來表徵自己的文字符號,又會變得非常不方便,例如,在台灣「9453」這個數字代表「阿童」,同樣的數字「9453」在美國代表「阿普」,在韓國「9453」則代表的是「金秘書」,這樣真的是非常的不方便阿!有沒有辦法讓全世界有一個共用的罪犯編號不會重複,讓 9453 就只能代表阿童呢?
於是,讓各語言的文字符號都能用一個數字代表它,而且這個數字是世界通用且不會重複的,就出現了所謂的 Unicode(萬國碼)這個編碼。Unicode 就像一個世界通用的大字典,收納了好幾百萬個全世界的文字符號,且每一個文字符號都有一個屬於自己的編號。不會同一個編號在台灣、美國、日本卻表示不同的文字符號。
Unicode 就像一個世界通用的大字典,收納了好幾百萬個全世界的文字符號,且每一個文字符號都有一個屬於自己的編號,在 Unicode 的官網 http://www.unicode.org/ 中列出了所有的 Unicode。
用來表示數值的不同方式:談談進位制
這裡我們要先稍微跳開一下文字編碼的部分,來談一下進位制。
因為電腦只認得數字(0
和 1
),因此不論是 ASCII 或 Unicode,都是把數字和文字符號間做一對一的轉換,因此都可以把文字轉換成對應到的數字,但是用來表示數字的方式則有很多種,我們把它稱為進位制,像是二進制、八進制、十進制和十六進制等等。
由於在 Unicode 中多數使用十六進制的方式來表示一個數值,因此在這裡先簡單說明進位制的概念和轉換。
不同進位制的表示
在這裡我們不會說明進位制之間轉換的計算方式,而是說明如何用 JavaScript 或工具進行數字間不同進位制的轉換,先來簡單瞭解不同進位制的表示方式:
二進制(Binary;bin):只用 0
和 1
這兩個數字表示一個數值的方式就稱為二進制,因此最後要給電腦看的數值最終都將轉為二進制(Binary)。在 JavaScript 中,如果你要告訴程式這是一個二進制的數字,需在數字的最前面加上 0b
。
八進位(Octal;oct):只用 0
, 1
, ... 7
這八個數字來表示一個數值的方式稱為八進制。在 JavaScript 中,如果你要告訴程式這是一個八進制的數字,需在數字的最前面加上 0
或 0o
。
十進位(Decimal;dec):這是平常使用的數字表示法,只用 0
, 1
, ... 9
這十個數字來表示一個數值的方式稱為十進制。在 JavaScript 中,如果你要告訴程式這是一個十進制的數字,你什麼都不必做,直接輸入的數字就是表示十進制。
十六進位(Hexadecimal;hex): 只用 0
, 1
, ..., 9
, A
, B
, C
, D
, E
, F
這十六個是數字來表示一個數值的方式稱為十六進制,其中 A 表示十進制中的 10
, B 表示十進制中的 11
,F 表示十進制中的 15
,以此類推。在色碼的表示上,也常使用十六進制的數字(如,#FF00AA
)來表示一個顏色。在 JavaScript 中,如果你要告訴程式這是一個十六進制的數字,需在數字的最前面加上 0x
。
使用十六進制的好處是,可以使用較少的位數就能表示更多的數值,例如十六進制的兩位數的
FF
就表示在十進制的需要用三位數表示的255
。
進位制(縮寫) | 前綴法(JavaScript) | 下標表示法 |
---|---|---|
二進位(Binary;bin) | 0b 11111110 | (11111110) |
八進位(Octal;oct) | 0 376, 0o 376 | (376) |
十進位(Decimal;dec) | 254 | (254) |
十六進位(Hexadecimal;hex) | 0x fe | (fe) |
使用 JavaScript 進行不同進位制間的轉換
將不同進位制的數值轉為十進位:Number()
在 JavaScript 中要把不同進位制的數值轉換成十進制非常容易,只需要在該數值前讓 JavaScript 知道該數值是什麼進位制(例如,0x
, 0b
)接著使用 Number()
函式即可。
在下面的例子中,9453
, 10010011101101
, 22355
, 24ed
這些不同進位制的數字在轉換後其實都是表示十進制的 9453
:
/* 使用 Number() 將不同進位制的數值轉為 10 進位 */
Number('9453'); // 回傳 9453,直接帶入就是 10 進制
Number('0b10010011101101'); // 回傳 9453,以二進制的的前綴法 0b 表示
Number('022355'); // 回傳 9453,以舊八進制的前綴法 0 表示
Number('0o22355'); // 回傳 9453,以新的八進制的前綴法 0o 表示
Number('0x24ed'); // 回傳 9453,以十六進制的的前綴法 0x 表示
你會發現,十六進位制(
24ed
)比起二進位制(0b10010011101101
),明顯只需要較少的位數就能表示同一個數值。
除了使用 Number()
函式外,使用 Number.parseInt(string, radix)
也能達到轉換的效果:
// 指定該內容要使用的進位制
parseInt('fe', 16); // 254, 0xfe = 254
parseInt('376', 8); // 254, 0o376 = 254
parseInt('11111110', 2); // 254, 0b11111110 = 254
若想進一步瞭解
Number.parseInt()
的用法,可參考parseInt()
在 MDN 上的說明。
將十進位轉換為不同進位制:toString()
如果想要將十進位的數值轉換成其他進位制的話,可以使用 Number.prototype.toString()
這個函式:
/* 使用 toString() 將十進位轉換成不同進位制 */
(9453)
.toString(2)(
// 回傳 '10010011101101',將 9453 轉換成 二 進制
9453,
)
.toString(8)(
// 回傳 '22355',將 9453 轉換成 八 進制
9453,
)
.toString(16); // 回傳 '24ed',將 9453 轉換成 十六 進制
進位制轉換工具
如果你想要看看自己轉換的結果正不正確,可以使用進位換算計算機這套小工具,我們平常用的是 10 進位的數字,所以只需要在十進位的地方輸入「9453」按下計算後,就會出現二進位的結果是「10010011101101」。
Unicode 的表示方式(U+<十六進制數值>
)
在瞭解不同數值間的進位制轉換後,來看看一般會怎麼樣來表示 Unicode 的數值。
前面提到過Unicode 就像一個世界通用的大字典,收納了好幾百萬個全世界的文字符號,且每一個文字符號都有一個屬於自己的編號。
而一般在 Unicode 中我們會用十六進制來表示某一個文字符號的編號,並且使用 U+<十六進制數值>
的方式來表示,例如 U+0061
就表示英文字母的 a
,其中的 0061
是十六進位制的數值,轉換成 10 進位的話是 97
,你可以發現這和當初的 ASCII 表示相對應的:
Number('0x0061'); // 回傳 97,將十六進制的 0061 轉成 10 進制
又例如,阿童的「阿」用 Unicode 來表示是 U+963f
,「童」則是可用 Unicode U+7ae5
表示。
碼點(Code Point)
概念上,我們會把這些文字符號所對應到的編號,稱作是 「碼點(Code Point)」,又稱作**「編碼位置」**。 在 Unicode 中指的是 U+
後面的十六進制數值,每個碼點都是唯一的。例如 a 的碼點是 0061
,「阿」的碼點是 963f
,「灣」的碼點是 7ae5
。
根據碼點的編號範圍,又可以分成「基本平面」和「輔助平面」。
基本名面和輔助平面
Unicode 編碼中碼點的可能範圍從 U+0000
一直到 U+10FFFF
超過 110 萬個文字符號,因此又可分為基本平面(BMP, Basic Multilingual Plane)和輔助平面(SMP, Supplementary planes or Astral planes)。
- 基本平面(BMP):碼點位置範圍從
U+0000
到U+FFFF
,這個平面放了最常見的文字符號。 - 輔助平面(SMP):碼點位置範圍從
U+010000
一直到U+10FFFF
,又稱為補充平面。
此外,原本 ASCII 中數字和文字符號的對應關係,可以直接沿用到 Unicode 中,也就是 ASCII 中 a 的編號會和 Unicode 中 a 的編號相同。
JavaScript 中提供的相關函式
keywords: String.fromCodePoint()
, String.prototype.codePointAt()
若想要查看某一個字的 Unicode 碼點,或者根據碼點反查是某一個字,在 JavaScript ES6 提供了 String.fromCodePoint()
, String.prototype.codePointAt()
這兩個函式,讓你可以在 Unicode 碼點和文字符號間相互轉換。
str.codePointAt()
和String.fromCodePoint()
是 ES6 提供的函式,若要對應回 ES5 的用法,則分別是對應回str.charCodeAt()
和String.fromCharCode()
。大部分的情況下 ES5 和 ES6 會得到一樣的結果,但若有使用到輔助平面文字符號(
U+010000
以上)時 ES5 則可能會產生錯誤,因此建議在支援 ES6 的情況下,使用 ES6 的函式以避免錯誤發生。
文字符號 --> Unicode 碼點(10 進位)
透過 String.prototype.codePointAt()
可以將一個文字符號轉換成 Unicode 碼點,但回傳的是十進位,因此一般會需要轉成 16 進位來表示:
// 文字符號 --> Unicode 碼點(10 進位)
String.prototype.codePointAt(<index>) // ES6 提供的方法
'A'.codePointAt() // 回傳 '65'(這是十進制)
(65).toString(16) // 回傳 41,將 65 轉成十六進制後,表示 A 為 U+0041
// 也可以一次把它轉成 16 進制
'A'.codePointAt().toString(16) // '41',表示 A = U+0041
'童'.codePointAt().toString(16) // '7ae5',表示童 = U+7ae5
'💩'.codePointAt().toString(16) // '1f4a9',表示 💩 = U+1f4a9
Unicode 碼點 --> 文字符號
使用 JavaScript ES6 的方法 String.formCodePoint()
則可以把碼點轉換為文字符號。如同前面文章「不同進位制的表示」所述,數字在表示時若為 16 進位,則開頭需加上 0x
來表示:
// Unicode 碼點 -> 文字符號
String.fromCodePoint(<num1> [, ...[, numN]]) // ES6 提供的方法
String.fromCodePoint(65) // A,使用 10 進位
String.fromCodePoint(0x0041) // A,使用 16 進位
String.fromCodePoint(0o101) // A,使用 8 進位
在 JavaScript 字串中顯示 Unicode 字元
除了透過 String.formCodePoint(<num>)
這種方式來將 Unicode 碼點轉換為文字符號外。在 JavaScript 中,可以直接使用 console.log()
函式就能將 Unicode 轉換成文字符號顯示出來。
其中根據不同的適用時機或場合有幾種不同的表示方式,包括:
- 只使用到 Unicode 基本平面時 (
\u<碼點>
) - 有使用到 Unicode 輔助平面時(
\u{ <碼點> }
) - 只使用到 ASCII 字符時(
\x<碼點>
)
只使用到 Unicode 基本平面時(\u<碼點>
)
- 使用:
\u<碼點>
- 適用範圍:Unicode 基本平面字符,也就是
U+0000
到U+FFFF
。
console.log('\u0041\u0042\u0043'); // 'ABC'
'臺' === '\u81fa'; // true
console.log('\u81fa'); // 臺
console.log('\u81fa\u7063'); // 臺灣
console.log('\u2661 \u81fa\u7063'); // '♡ 臺灣'
❗ 若在
\u
使用了輔助平面文字符號的碼點時會產生錯誤的結果。
有使用到 Unicode 輔助平面時(\u{ 碼點 }
)
- 使用:
\u{碼點}
- 適用範圍:所有 Unicode 字元,特別是有使用超過
U+FFFF
的輔助平面文字符號
為了支援輔助平面的字元,在 JavaScript ES6 中引進了新的 Unicode 碼點跳脫序列(Unicode code point escapes),透過將碼點放在大括號 {}
內,也就是 \u{碼點}
就能正確識別,許多常見的 emoji 符號都是屬於輔助平面內的文字符號:
// 使用 2 位數的十六進制(一個位元組)
console.log('\u{41}\u{42}\u{43}'); // 'ABC'
// 使用 4 位數的十六進制(兩個位元組)
console.log('\u{0041}\u{0042}\u{0043}') // ABC
// 使用超過 4 位數以上的十六進制
console.log('\u{1F4A9}'); // '💩' U+1F4A9
console.log('\u{1F923}'); // '🤣' U+1F923
console.log('\u{1F436}') // '🐶' U+1F436
只使用到 ASCII 字符時(\\x)
- 使用:
\x<碼點>
- 適用範圍:
U+0000
到U+00FF
,只用到 Unicode 兩位數的十六進制時,也就是一個位元組時 - 舉例來說,
U+0041
=\x41
console.log('\x41\x42\x43'); // 'ABC'
統整一下
A = U+0041 = \x41 = \u0041 = \u{41} = \u{0041}
也就是下面會得到一樣的結果
console.log('\x41'); // 'A'
console.log('\u0041'); // 'A'
console.log('\u{41}'); // 'A'
console.log('\u{0041}'); // 'A'
在 CSS 中使用 Unicode
如果想要在 CSS 中使用 Unicode,可以使用前輟 \<碼點>
來進行跳脫:
.content {
display: inline-block;
background-color: steelblue;
padding: 100px;
color: white;
}
.content::before {
content: '\0041'; // A, 輸入 16 進位制的 Unicode
// content: '\570B'; // 國
// content: '\00A9'; // ©
}