精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

關于編碼的那些事——前端應該了解的字符編碼

開發 前端
數據庫存儲時使用了不同的表示方式存儲同一含義的字符,會導致檢索失敗。MySQL 存儲時也要考慮使用 utf8mb4_unicode 編碼格式,才能正確存儲表情符。

背景

工作中我們會不會遇到以下問題:

  • 可能你知道 JavaScript 中 '??'.length = 2,但 '????????'.length 呢?可以自行實驗下,思考為何如此
  • 困惑于 Unicode 和 UTF-8 的關系?
  • 學計算機時會遇到這樣的提問:一個漢字是幾個字節?
  • 讀取二進制數據時,為何有大端序小端序的分別?
  • 為何 UTF-8 文件最好存儲為無 BOM 頭格式?
  • 數據亂碼時總會看到“錕斤拷”、“燙燙燙”,這是什么鬼?

這些問題都涉及計算機中基礎的知識點:字符集及字符編碼的概念,希望通過這篇文章可以讓你徹底清晰的理解這些問題。

字符集與字符編碼

首先通過 wiki 中關于字符編碼(Character_encoding)的定義來引入幾個概念:

Character encoding is the process of assigning numbers to graphical characters, especially the written characters of human language, allowing them to be stored, transmitted, and transformed using digital computers.

The numerical values that make up a character encoding are known as "code points" and collectively comprise a "code space", a "code page", or a "character map".

字符編碼是將數字分配給圖形字符的過程,特別是人類語言的書寫字符,使它們能夠使用計算機進行存儲、傳輸和轉換。組成字符編碼的數值稱為“碼位”,它們共同組成“代碼空間”、“代碼頁”或“字符映射”。

這里所說的代碼頁(Code Page)其實就可以理解為編碼字符集(coded character set),如 Unicode、GBK 字符集等。

簡單來說,字符編碼就是將字符映射為固定的碼位值,存儲在對應的編碼字符集中。在不同的字符集中,同一個字符的碼位不同。其中碼位也有翻譯成碼點或者內碼。

ASCII 字符集

我們知道在計算機存儲數據時要使用二進制進行表示。而最初計算機只在美國使用,因此人們要考慮如何使用二進制來表達 52 個英文字母(包括大小寫)、阿拉伯數字(0-9)以及常用的符號(如! @ # $ 等)。

于是便有從電報碼發展而來的 ASCII(American Standard Code for Information Interchange,美國信息交換標準代碼)(發音 /??ski/)編碼。它定義了英文字符和二進制的對應關系,一直沿用至今。

它采用了單字節編碼方案(SBCS) ,一個字節的首位 bit 為 0,用其余 7 個 bit 來表示 128 個字符(范圍 0x00-0x7F)

  • 其中 0-31 和 127 (0x00-0x1F 和 0x7F) 為控制字符,共 33 個。這些字符是不可見的,用于進行終端的換行、響鈴、刪除等動作。
  • 32-126 (0x20-0x7E) 位可見字符,共 95 個,存儲了空格、0-9 十個阿拉伯數字、52 個大小寫英文字母,以及標點、運算符號等。

雖然現代英語使用 128 個字符就足夠了,但表示其他語言就遠遠不夠了。因此當 ASCII 進入歐洲后,又被擴展為了 EASCII(Extended ASCII),將 7 bit 擴展為 8 bit,并且前 127 個編碼含義和 ASCII 保持一致。

  • 但 256 個字符依舊無法解決眾多使用拉丁字母的語言(主要是歐洲語言)問題。于是又擴展出了 15 個 ISO 8859 字符集。舉幾個字符集作為了解

ISO/IEC 8859-1 (Latin-1) - 西歐語言

ISO/IEC 8859-2 (Latin-2) - 中歐語言

ISO/IEC 8859-3 (Latin-3) - 南歐語言

ISO/IEC 8859-4 (Latin-4) - 北歐語言

...

中文字符集

前面講到拉丁文所使用是 ASCII 和 EASCII,但在亞洲——主要是中日韓(CJK)——光常用漢字就 6000 多個,而漢字總共有 5、6 萬之多,單靠一個字節是遠遠沒辦法做到的。因此聰明的中國人(也可能是日本人)就想到使用雙字節編碼(DBCS) 來表示一個漢字,這樣理論上 2 個字節可以表達 65535 個字符(當然很理論,實際上要少很多)。這樣就可以表示大部分常用字符了,接下來就要講到和中文有關的 GB 系列(如 GB2312、GBK、GB18030)的字符集了。

其實 GB 就是“國標”漢語拼音的首字母,而 GBK 就是“國標擴展”的意思,而 GB18030 是在保留 GBK 編碼基礎上再度擴展為可變的 4 字節編碼空間的編碼規范。

這里我們著重介紹下 GB2312 的編碼結構。

GB/T 2312

GB2312 全稱《信息交換用漢字編碼字符集·基本集》,收錄了 6763 個漢字,682 個拉丁、希臘、日文假名等字符。就將其中漢字覆蓋了大陸 99.75% 的使用頻率,足夠大部分的場景使用的。

同時還把 ASCII 中標點符號、阿拉伯數字、英文字符用雙字節收錄在內,這里為了區別于 ASCII 中的字符,就將其做成了與漢字等寬的正方型效果(即拉丁字母的兩倍寬),以表示和編碼存儲方式一一對應。因為字符編碼和字寬的對應關系,我們這就稱這類字符為「全角」字符,而 ASCII 中的字符則為「半角」字符。(這種叫法源于日本,因為在日本中“角”有“方塊”的意思)

接下來看看如何編碼的。規范將收錄的漢字分成 94 個區,每個區又包含 94 個漢字,用所在的區位來表示一個字符(這種方法也稱為區位碼) 。每個字符用 2 個字節表示,其中第一個字節稱為「高位字節」表示分區號,第二個字節稱為「低位字節」表示區段內的碼位。另外實際 GB2312 僅用了 87 個區,88-94 區保留待擴展。

為了避開 ASCII 中前面的 31 個不可見控制符和空格,在區位碼基礎上 +32(0x20),這樣獲得的編碼就稱為 ISO-2022 國標碼。

但是實際使用時,英文字符和漢字混用的情況很常見,國標碼僅避讓開了控制字符,依然和 ASCII 的英文字符有重合,因此決定在國標碼的基礎上將單字節的最高位 bit 存為 1,即在國標碼上 +128(0x80),也就是區位碼上 +160 (0xA0),這樣就完美的避讓開了 ASCII 字符區間。這樣種編碼就叫做 EUC-CN 機內碼。

EUC-CN 機內碼 = 區位碼 + 160

而目前 GB2312 所采用的就是 EUC 這種主流編碼方式,以便于兼容 ASCII 碼。同時也可以根據最高位 bit 來判斷是讀取 1 個字符(ASCII)還是 2 個字符來進行解析。在 GB2312 內,高位字節范圍 0xA1-0xF7(01-87 區+160 或 0xA0),低位字節范圍 0xA1-0xFE(01-94 + 160 或 0xA0)。

以“節”為例:

  • 區位碼是 29-58
  • EUC 編碼就是 <29+160, 58+160> = <189, 218>,
  • 十六進制就是 <0xBD, 0xDA>。

GBK/GB18030

但是新的問題來了,GB2312 僅收入了 6763 個常用的漢字,一些在標準推出以后的簡化字(如 啰)、港澳臺使用的的繁體字、某些領導人的名字(朱镕基的镕),以及各地區戶籍中用到奇奇怪怪的名字、古籍中的漢字都沒法正確表示。因此在 GB2312 的基礎上又擴展出了 GBK 和 GB18030,并且向下兼容(嚴格來講 GB18030 完全兼容 GB2312,基本兼容 GBK)。

GBK 的基本原理就是繼續擴展了 GB2312 中未使用的雙字節空間,字符多達 23940 個。而 GB18030 則更為激進,干脆將存儲空間變成可變的 1、2、4 字節,理論上可以存儲 161 萬個字符,完全涵蓋 Unicode 范圍。同時 GB18030 還收錄了中日韓、繁體字、少數民族文字等多種語言。

Big5

于此同時,海峽對岸的程序員也發明出了一套自己的繁體中文標準,由于起初項目名稱是「五大中文套裝軟件」,因此稱為 Big5,也叫大五碼。記得當年想要玩中國臺灣“流入”的游戲,MagicWin 這類的轉碼軟件是裝機必備。

Unicode 字符集

隨著世界各個地區編碼方式越來越多,給不同地區間數據傳輸造成非常大的困難,比如一個從中國發送到日本郵件的,在不知道其編碼的情況下,就會出現亂碼。因此終于有人開始想用一種更加通用的方案,來涵蓋世界上所有的字符和符號——這就是 Unicode,如其名字本身的含義一樣。

首先 Unicode 也采用了 16 位的編碼空間,即 2 個字節表示一個字符,用 "U+"接 4 個十六進制數字表示(例如U+4AE0),每個字符分配一個唯一的「碼點」(CodePoint,也稱碼位)。同時又定義了 17 個編組,每組稱為一個「平面」(Plane)。這樣字符范圍從 0x00000 - 0x10FFFF,這樣就可以最多表示 17 * 65536 也就是 100多萬個字符,完全可以存儲全世界所有的語言和符號了。

這其中將常用的字符存放在第一個編組,即 0 號平面(Plane 0),也稱之為「基本多文種平面 」(BMP - Basic Multilingual Plane),其范圍是 0x0000 - 0xFFFF 。而 1-16 號平面被稱為「輔助平面」(Supplementary Planes),用于存儲很少使用的文字或者圖形符號(emoji 表情符號就存于這些平面),其范圍是 0x10000-0x10FFFF。

圖片圖片

Unicode 字符表的范圍涵蓋的字符和符號非常廣,不僅收錄了表情符號、麻將、音樂符號等表意符號外,還收錄了一些早已不再使用的文字,比如甲骨文、古埃及的圣書體,甚至收錄了為影視劇而創造的語言——克林貢語。并且還在持續更新中,截止此文完成,最近的一次更新是在2022年9月。

但是這里有個比較嚴重的問題:

  1. 不同碼位的字符使用不同的字符長度,但計算機無法知道到底是按照 2 個還是 4 個字符解析;
  2. 即便只使用英文字符前一個字節也必須是 0,嚴重浪費了空間。

由于這個問題導致 Unicode 的在初期完全沒法推廣,直到互聯網的普及,急需一種對 Unicode 字符的編碼方式出現,這就是 ****UTF(Unicode Transformation Format)。在 UTF 中又出現了 UTF-8、UTF-16、UTF-32 這些不同的編碼方式。

UTF-32

首先人們想到的就是干脆所有字符直接用 4 個字節存儲,不足 4 位的就補 0 代替。但是這樣簡單粗暴,尤其僅使用英文的數據,會造成了空間的極大浪費。

比如拉丁字母 A 的 Unicode 碼點為 U+0065,十六進制為 0x41,如果使用 UTF-32 編碼就是 0x00000041。

于是人們又開始設計一種可以節省空間的編碼方式——UTF-8。

UTF-8

UTF-8 最大的特點是可變長編碼,它使用 1-4 個字節表示一個字符。它的編碼方式如下:

  1. 由于 Unicode 在 0+0000-0+007F 范圍和 ASCII 完全相同,因此使用單字節表示,這樣 ASCII 和 UTF-8 的編碼一樣,是完全兼容的。
  2. 在大于 0+007F 的字符時,是由 1 個前導字節(leading bytes)和 n 個(n >=1 )尾字節(trailing bytes)的多字節結構組成。前導字節從最左側起用 1來表示這個字符有多少位,直到遇到 0 為止。尾字節前兩位都是 10,其余的 bit 位就是可用編碼空間。
  1. 比如,第一個字節是 0xxxxxxx,就表示該字符只有 1 個字節;
  2. 第一個字節是 110xxxxx,表示該字符有 2 個字節,第二個字節是 10xxxxx;
  3. 第一個字符是 1110xxxx,表示該字符有 3 個字節,后面字節分別是 10xxxxx 10xxxxx;
  4. 第一個字節是 11110xxx,表示該字節有 4 個字節,后面字節分別是 10xxxxx 10xxxxx 10xxxxx。

這里 x 就表示可用的編碼空間,這也就是 UTF-8 中 8 表示至少 8 位表示一個字符,同時也是以 8 位為一組實現可變字節的編碼方式。(這里之所以跳過了 10xxxxxx,是因為 10 表示尾字節)

以下就是完整編碼方式,當然 5、6 字節的編碼方式不會出現,因為已經遠超 Unicode 最大碼點范圍 U+10FFFF 了。

圖片圖片

接下來我們用“兔” 和 emoji “??” 來舉例,來說明下 UTF-8 是如何編碼的。

兔:

  1. 碼點為 U+5154
  2. 二進制表示為 101000101010100,
  3. 從末尾按 6 個 bit 分組 101 000101 010100
  4. 需要 3 個字節,則開頭插入 1110,中間不足 8 位用 0 補足,剩余前面插入 10,則得到 11100101 10000101 10010100
  5. 轉為十六進制,E5 85 94

?? 的 emoji

  1. 碼點為 U+1F430
  2. 二進制為 11111010000110000
  3. 從末尾按 6 個 bit 分組 11111 010000 110000
  4. 第一部分為 5 位,插入 1110 位后占 9 位,超出一個字節,所以前面補一個字節,用 4 個字節表示。不足 8 位的用 0 補足。則得到 11110000 10011111 10010000 10110000
  5. 轉為十六進制,F0 9F 90 B0

由于 JS 中 encodeURI 是按照 UTF-8 進行百分號編碼,因此感興趣的同學可以進行驗證下

encodeURI('兔'); 
// %E5%85%94

encodeURI('??');
// %F0%9F%90%B0

除了相對于 4 字節編碼更省空間外,UTF-8 編碼還以下比較優秀的特性:

  • ASCII 是 UTF-8 的一個子集,一個純 ASCII 字符串也是一個合法的 UTF-8 字符串。因此現存的 ASCII 數據可以不經修改即可使用。
  • 單字節范圍 0x00-0x7F,而多字節的前導字節范圍總是在 0xC0-0xFD,尾字節都在 0x80-0xBF 中,三者完全沒有重疊。通過范圍就可以確定字節的類型,如果字節流在傳輸時中斷,不完整的字節也不會被解碼。這樣數據有損壞、丟失,影響的范圍也會很小。試想下,如果有一個按照每 4 個字節為一組的編碼方法,如果丟失了第一個字節的數據,那么繼續按照 4 字節一組的方式讀取數據,后面的數據都將無法讀取。
  • 另外由于前導字節和尾字節范圍互不重疊,即便從一個字符的某個中間字節開始讀取,也不會錯把它和下一個字符拼接錯。這種特性也叫自同步碼。比如,“兔子”的編碼為 E5 85 94 / E5 AD 90,你不會錯把 85 94 E5 或者 AD 90 當做一個字符,因為根本不存在這樣規則的 UTF-8 編碼。
  • 僅通過首字節就能確定整體字節長度,而無需等待下一個字節的讀取。
  • 理論 6 個字節可以存放 2^31 個字符,即 21 億個字符!即便是 4 字節也可以存放 200 多萬個字符。其范圍已超 Unicode 的容量了。
  • UTF-8 中未使用 0xFE 和 0xFF。
  • UTF-8 字節串的排列順序是固定的,和系統無關。沒有字節序(即是按大端序還是小端序的先后順序)的問題。因此也無需使用 BOM 頭(雖然其 BOM 頭是 0xEF 0xBB 0xBF,但僅表示 UTF-8 編碼格式,不表示字節順序)。

雖然 UTF-8 有諸多優勢,但是也有缺點:

  • 中日韓文字至少需要 3 個字節存儲,比 GBK 這類雙字節存儲要多一個字節。
  • 由于其是變長字節,一個字符可能是 1、2、3 個字節,因此當計算一組字節中包含多少個字符,就要從頭遍歷,才能確定到底有多少個字符。當我們需要獲取指定位置的字符時,依然要從頭遍歷。這也為程序的實現帶來了很大的麻煩。

因此一些程序在內部處理字符串時,就使用了另外一種編碼方式 UTF-16,其中 JavaScript、Python 就使用了這種編碼方式來存儲字符串。

UTF-16

UTF-16 也采用了變長的編碼方式。使用 2 個或者 4 個字節表示一個字符。其規則是:

  • 在基本平面 BMP 內(U+0000 至 U+FFFF)的字符用 2 個字節表示;
  • 在輔助平面內(U+10000 至 U+10FFFF)的字符用 4 個字節表示。

其中 BMP 的字符直接使用 Unicode 碼點對應即可,比如“兔”的碼點 U+5154,UTF-16 編碼就是 0x5154。

可是 BMP 外,要如何使用 4 個字節表示呢?假如我們使用 UTF-32 的方式,直接使用碼點映射會遇到什么問題?還以“??”為例,其碼點是 U+1F430。如果直接映射為 0x0001F430,頭 2 個字節 0x0001 對應的字符在 BMP 中已經存在,這樣在讀取數據時就無法區分到底是按照 2 字節解析,還是按照 4 個字節解析了。

幸而在 BMP 中,從 U+D800 到 U+DFFF 是一個永遠保留不做映射的空段,于是 UTF-16 將輔助平面內的字符 —— 共需要 20 個 bit 表示(由 0x10FFFF - 0x100000 = 0xFFFFF 計算得來) —— 拆分成 2 段,前 10 個 bit 位映射到 U+D800 - U+DBFF(正好 1024 個),后 10 個 bit 映射到 0xDC00 - 0xDFFF(也正好 1024 個)。這樣剛好就可以在范圍互不重復的情況下,用 4 個字節表示一個字符了。

這種用 4 個字節表示的方式,就被稱作「代理對」(Surrogate Pair),高位的 10 bit 被稱為「前導代理」(lead surrogates),低位 10 bit 被稱為「后尾代理」(trail surrogates)。

其具體的算法如下:

  1. BMP 內字符,直接使用碼點作為對應的字節,長度為 2 個字節。
  2. 輔助面內的字符
  1. 碼位減去 0x10000
  2. 高位的 10 bit 的值加上 0xD800 ,得到前導代理
  3. 低位的 10 bit 的值加上 0xDC00 ,得到后尾代理

用公式計算如下

// 高位
H = Math.floor((c-0x10000) / 0x400)+0xD800

// 低位
L = (c - 0x10000) % 0x400 + 0xDC00

我們依舊使用“??”來舉例,看下 UTF-16 是如何進行編碼的

??:

  1. 碼點為 U+1F430
  2. 減去 0x10000,為 0xF430
  3. 二進制為 1111010000110000
  4. 前 10 bit 111101,十六進制 0x3D。后 10 bit 0000110000,十六進制 0x30
  5. 分別加上 0xD800 和 0xDC00,得到 0xD83D 和 0xDC30
  6. 最終得到 D8 3D DC 30

JS 中 escape 方法(已不推薦,這里僅驗證用)返回的就是 UTF-16 編碼,可以進行驗證

escape('??')
// %uD83D%uDC30

同時 ES6 中支持用 Unicode 碼點和 UTF-16 編碼表示一個字符,也可驗證

'\u{1F430}' == '\uD83D\uDC30'
// true

由于前導代理、后尾代理、BMP 中的有效字符的碼位,三者互不重疊,因此在檢查字符時,可以很容易的確定字符的邊界。這也意味著 UTF-16 也是一種自同步的編碼方式,這點和 UTF-8 是一樣的。

UTF-16 相對于 UTF-8 更容易進行隨機訪問和索引,是因為 UTF-16 中每個字符都使用固定的2個或4個字節表示,因此可以通過簡單的數學運算來快速計算出每個字符的位置。而 UTF-8 需要解析整個數據流才能確定每個字符的位置,這使得 UTF-8 在進行隨機訪問和索引時比 UTF-16 更加復雜和耗時。

UCS-2

在談及 UTF-16 時,通常會涉及到 UCS-2 的概念,這里著重講下兩者的區別。

UCS-2 是 Universal Character Set coded in 2 octets 的簡稱,由于 1989 年 UCS 標準發布時,只有 Unicode 基本面字符,因此使用 2 個字節就可以表示一個字符了。而 1996 年 UTF-16 發布時,已經擴展出了輔助平面,可使用代理對方式表示輔助平面的字符了,因此 UTF-16 已明確表示是 UCS-2 的超集。所以目前 UCS-2 已經是過時的叫法了,UTF-16 已經取代了 UCS-2 的概念。

如果某個程序說自己支持 UCS-2 可能就意味著僅支持 BMP 內的字符集。

大端序和小端序

將字符轉換為 UTF-16(或 UTF-32) 后,在使用時需要讀取并存儲在內存中。以“乙”字為例,轉成 UTF-16 后,編碼為 0x4E59,需要用 2 個字節來存儲——0x4E 0x59。

如果 4E 在前,59 在后,我們就稱作為大端序(Big-Endian)

內存地址     內存值 
0x00000000  4E
0x00000001  59

反之 59 在前,4E 在后,就稱之為小端序(Little-Endian)

內存地址     內存值 
0x00000000  59
0x00000001  4E

之所以會有這樣不同的存儲方式,是因為有的系統或者處理器,在處理多字節數據時,會從低位內存地址到高位內存地址的順序讀取數據,因此為了保證讀取后數據的正確,就采用了不同字節序(Endianness)的方式。

為了標識一段數據的字節序,通常使用字節順序標記 (Byte-Order Mark,BOM)來進行標識。通常 BOM 會出現在文件、字節流的頭部,用來標識接下來數據的字節順序。

在 UTF-16,0xFEFF 表示大端序,0xFFFE 表示小端序。

我們來驗證下,使用 VSCode 安裝 HexEditor 插件,然后新建一個空白文件,輸入“乙”和“??”,然后分別使用兩種編序,通過 HexEditor 查看。

UTF-16 BE

圖片圖片

圖片圖片

UTF-16 LE

另外 UTF-8 也存在 BOM 頭,值為 0xEF 0xBB 0xBF。但它只用來標識文件的編碼方式,而不用來說明字節順序。因為 UTF-8 本身有著固定的編碼順序,字節序對于 UTF-8 來說毫無意義。

在實際使用中,也應該盡量避免帶 BOM 頭的 UTF-8,因為 BOM 頭可能會影響一些程序的解析器(如 Unix下的Shebang)的處理。另外由于 UTF-8 是一種稀疏的編碼,很大一部分可能的字符組合不會產生有效的 UTF-8 文本,因此許多程序的在解析 UTF-8 文件時,使用啟發式的分析方法可以很有把握地檢測出文件是否使用 UTF-8,而無需加入 BOM。

錕斤拷燙燙燙

“錕斤拷”和“燙燙燙”恐怕是程序界中最經典的故事(事故)之一了,了解了前面的編碼知識,就會很容易理解它的由來了。

圖片圖片

錕斤拷

由于 Unicode 字符集在不斷更新中,因此會出現 A 系統發送的字符,在 B 系統中無法識別的情況。于是 Unicode 規定對于無法識別的字符,一律使用 ?(0xFFFD) (Replacement character) 字符來代替。而 0xFFFD 在 UTF-8 編碼下為 0xEF 0xBF 0xBD,當多 ? 出現時,就會產生連續的 0xEF 0xBF 0xBD 0xEF 0xBF 0xBD。

如果這些字符又被使用了 GB 編碼的程序中打開,就會按照 GB 雙字節編碼將其解析。這樣剛好就對應了 「0xEFBF 錕」 ,「0xBDEF 斤」,「0xBFBD 拷」 這幾個字。

燙燙燙

C\C++ 編譯器在 debug 模式下,引入的一種內存保護機制,會給特定的內存賦一個特定的初值。其中未被初始化的棧內存,會被寫入 0XCC。當連續的 0xCCCC 在 GB 編碼下就是「0xCCCC 燙」了。

當然一千人眼中有一千個哈姆雷特,程序員也是如此。比如中國臺灣是“嚙踝蕭”,而日本是“フフフフフフ”了。

JavaScript 與 Unicode

薛定諤的 length

前面我們提到 JavaScript 中 '??'.length = 2,要說明這個問題之前,先簡單介紹下 JavaScript 與編碼的歷史。

1990 年 UCS-2 編碼發布,1995 年 JavaScript 誕生,第一個 JS 解釋器被使用,1996 年 UTF-16 發布。從時間線上來看 JavaScript 解釋器只能采用了 UCS-2 的編碼方式。而 length 統計的是代碼單元而非真正的字符數量,因此按照 UCS-2 的編碼方式,每個字符由 2 個字節構成,即兩個 2 字節組成字符的 length = 1。

而 ?? 是輔助平面的字符,其 UTF-16 的編碼為 D8 3D DE 01,因此 JavaScript 會把它當做 0xD83D 和 0xDE01 兩個字符。這也就是 length = 2 的原因。

除 length 以外,JavaScript 中和字符串處理相關的函數,大都存在這類問題,如:

var s = "??";

s.length // 2
s.charAt(0) // '\uD83D'
s.charAt(1) // '\uDC30'
s.charCodeAt(0) // 55357
s.charCodeAt(1) // 56368
var s = "??子";

s.substring(1); // '\uDC30子'
s.slice(1); // '\uDC30子'

/^.$/.test('??') // false

ES6 的字符處理

但在 ES6 中,大大增強了對 Unicode 的支持,提供了新的方法解決了以上問題。

  • 為了保持兼容,length 屬性還是原來的行為方式。為了得到字符串的正確長度,可以采用如下的方式
[...str].length;

// or
Array.from(str).length
  • 原 Unicode 表示字符法,僅支持 \uxxxx 的表示,僅支持 \u0000 - \uffff 范圍。如:“兔”也可以用 \u5154 表示
var a = '\u5154';
a == '兔' // true

而超出 \uffff 的部分需要使用雙字節方式表示,如:“??”需要用 \uD83D\uDC30 表示,而不能用碼位值 \u1F430 表示

var a = ' \uD83D\uDC30';
a == '??' // true

var a = '\u1F430';
// '?0'

而在 ES6 中只要將碼點放入 {} 中,即可正確表示其含義

var a = '\u{1F430}';
// ??
  • ES6 新增了幾個專門處理 4 字節碼點的函數,如:

String.fromCodePoint():從 Unicode 碼點返回對應字符

String.prototype.codePointAt():從字符返回對應的碼點

String.prototype.at():返回字符串給定位置的字符

  • ES6 正則提供了 u 修飾符,對正則表達式添加 4 字節碼點的支持。
// '\u{1F430}'
/^.$/.test('??'); // false

/^.$/u.test('??'); // true

var s = '\u0065\u0323\u0301'; // '??'
/^.$/.test(s); // false
/^.$/u.test(s); // false

length 依舊不準的合成字符

上面解釋了為什么 length=2,是否意味著 length 非 1 即 2 呢?別急,我們運行下 '????????'.length

圖片圖片

居然 length=11,為什么整整齊齊的一家人要這么長?這到底發生了什么?

要解釋這個問題,就要講到 Unicode 的**合成字符** (combining character)。

在一些語言中,會在字符的上面添加一些符號,以表示不同的發音或表示不同的含義。例如:漢語拼音有 ā 的音標、ü 上面的兩點,法語和西班牙語中的 é 表示重音,越南國語字中的 ? 表示一個字母。

而在 Unicode 中有兩種表示方法

  1. 使用一個獨立的碼點,例如 ? 的碼點為 U+00D4。
  2. 使用基本字符+**附加符號**組合的方式,如 O 的碼點是 U+004F,揚抑符 ? 的碼點是 U+0302,組合后同樣展示 ?,其表意與第一種相同。
'\u00d4'
// ?

'\u004f\u0302'
// ?

這兩種方式表示雖然在視覺和語義上含義相同,但是在 js 中卻無法理解

'\u004f\u0302' == '\u00d4'
// false

因此在 ES6 中提供了 String.prototype.normalize(),用來將字符的不同表示方法統一為同樣的形式,稱為 Unicode 正規化(該方法有局限性,后文會講)。

'\u004f\u0302'.normalize() == '\u00d4'
// true

'\u004f\u0302'.normalize().codePointAt(0).toString(16)
// 'd4'

那么回到 '????????'.length 問題上來,這個 emoji 就使用了合成符號表示(這個表情沒有單獨的碼點),其組合是由 7 個 Unicode 組合而成,分別是 ['??', '', '??', '', '??', '', '??'], 使用了 U+ 200D 零寬連字符(Zero-Width Joiner,ZWJ)將四個 emoji 鏈接。

這類表情也被稱為 Emoji ZWJ Sequences 。

圖片圖片

而 ?? ?? ?? ?? 的 length 分別為 2,而零寬連字符的 length 為 1,因此最終 length 就是 24 + 31 = 11。

通過下面的方式也可以驗證其組合

[...'????????']
// ['??', '', '??', '', '??', '', '??']

'\u{1F468}\u{200D}\u{1F469}\u200D\u{1F467}\u200D\u{1F466}'
// ????????

當然這里想通過 normalize() 判斷是否等價是不行的,首先 ???????? 沒有獨立的碼點, 其次該方法目前不能識別三個或三個以上字符的合成。

[...'????????'.normalize()];
// ['??', '', '??', '', '??', '', '??']

// '\u0065\u0323\u0301' => '??'
[...'??'.normalize()]
// ['?', '?']

合成字符的大麻煩

上面提到關于 Unicode 的問題不僅限于 JavaScript,在 Python、Java 等語言中也會遇到。通常這類合成符號在實際開發中會造成很多棘手的問題。

input 的 maxlength

在設置 input 的 maxLength 屬性時,就會遇到不同瀏覽器的差異。

<input maxlength="10">

Chrome 將 ???????? 粘貼后,會按照 String.prototype.length 進行裁剪,多出的字符會被裁剪掉

圖片圖片

例子在這里

Safari(PC 16.3+Moblie)則會按照實際字符個數計算

圖片圖片

在華為默認的瀏覽器測試,行為和 Chrome 的相同

圖片圖片

該問題在開發多語言業務中,可能會更加明顯(如后臺的字符數限制、前端展示等)。如果希望正確計算需要使用三方庫,例如:https://github.com/orling/grapheme-splitter (拋磚引玉,未驗證覆蓋情況)。

帶音標的文字

在一些場景下,會有多個附加音標修飾一個字符的情況。例如 '??' 是由字母“e” \u0065、尖音符 “??” ****\u0301 、 下句點 ****“??” \u0323 ****組成。其中后兩個音標的順序可互換, 甚至也可以是 '?' + '?' 或者 'é' + '?' 的組合。

'\u0065\u0301\u0323'; // ??
'\u0065\u0323\u0301'; // ??
'\u1eb9\u0301'; // '?' + '?' 
'\u00e9\u0323'; // 'é' + '?'

圖片圖片

圖片

這時要使用正則匹配字符變得異常復雜。雖然 Unicode 屬性轉義表達式(Unicode property escapes),但可惜的是ES2018 以前的版本并不支持,因此可以考慮使用 XRegExp 來實現。

// 例子在這里
var reg = XRegExp('\pL\pM*', 'g');
XRegExp.match('??', reg);
// ["??"]

其中 \pL 和 \pM 的含義如下:

  • \p{L} or \p{Letter}: any kind of letter from any language,匹配一個字母
  • \p{M} or \p{Mark}: a character intended to be combined with another character (e.g. accents, umlauts, enclosing boxes, etc.). 匹配附加符號

雖然在 ES2018 中引入了 Unicode 屬性轉義符,但在瀏覽端上依然要考慮使用 XRegExp 來實現,當然可以考慮在服務端處理,因為 Python 3.6、Perl 5.24 、Ruby 2.4 、.NET 4.6、Go 10 等已經支持這些表達式了,當然不同語言的支持程度可能略有不同。

數據庫中存儲表情

另外在數據庫存儲時使用了不同的表示方式存儲同一含義的字符,會導致檢索失敗。MySQL 存儲時也要考慮使用 utf8mb4_unicode 編碼格式,才能正確存儲表情符。

總結

最后關于 Unicode 字符編碼模型,可以概況為四個層級:

  1. 抽象字符庫(ACR, Abstract Character Repertoire):即要編碼的字符,比如某些字母表和符號集。這一層級會將語言中的字符進行抽象。
  2. 編碼字符集(CCS, Coded Character Set):從抽象字符庫到一組非負整數(即代碼點)的映射。這一層級可以理解為字符到 Unicode 碼點映射的過程。
  3. 字符編碼形式(CEF, Character Encoding Form):從代碼點到特定寬度(比如32-bit整數)的代碼單元序列的映射。這一層級理解為 UTF-8、UTF-16、UTF-32 的編碼過程。
  4. 字符編碼方案(CES, Character Encoding Scheme):從代碼單元序列到字節序列的映射。這一層級就涉及到了如大端序、小端序的設計和存取過程。

參考資料

責任編輯:武曉燕 來源: ELab團隊
相關推薦

2017-11-28 15:24:14

ETA配送構造

2012-07-13 00:03:08

WEB前端開發WEB開發

2017-11-03 13:43:24

云計算Saas信息化

2017-08-28 15:30:49

Android編碼器編碼

2019-11-19 16:45:09

Web前端開發編碼原則

2018-08-23 08:21:54

TensorFlow機器學習人工智能

2010-07-27 11:29:43

Flex

2016-05-12 15:51:08

前端開發字符編碼

2019-12-10 08:00:46

Kata容器Linux

2015-08-13 10:54:46

2015-09-14 09:28:47

2009-02-19 10:21:00

路由多WAN口

2021-03-18 16:05:20

SSD存儲故障

2022-10-27 10:29:15

2022-11-04 07:57:59

編程編碼編譯器

2019-02-19 09:34:53

工業物聯網IIOT物聯網

2020-08-10 15:30:24

XDR網絡安全網絡威脅

2012-05-01 08:06:49

手機

2014-07-31 17:13:50

編碼程序員

2023-02-16 18:03:28

點贊
收藏

51CTO技術棧公眾號

久久精品免费在线观看| 美女爽到呻吟久久久久| 日韩欧美国产一区在线观看| av在线免费观看国产| 五月激情婷婷综合| 美女网站在线免费欧美精品| 欧美精品在线观看| 波多野结衣福利| www欧美在线观看| 欧美日韩亚洲成人| 亚洲一区二区三区免费观看| 精品人妻一区二区三区换脸明星 | 亚洲精品69| 亚洲国产欧美一区二区三区丁香婷| 美女被啪啪一区二区| 国产精品自产拍| 日韩天天综合| 久久高清视频免费| 色屁屁草草影院ccyy.com| 中文久久电影小说| 欧美区在线观看| 激情五月开心婷婷| 国产探花视频在线观看| 国产精品国产三级国产三级人妇| 久久久久久欧美精品色一二三四| 性一交一乱一透一a级| 男人的天堂亚洲一区| 91极品视频在线| 九九热精品在线观看| 第一会所sis001亚洲| 精品网站999www| 波多野结衣办公室双飞| 国产精品美女久久久久人| 色乱码一区二区三区88| 国产3p露脸普通话对白| 日本在线视频中文有码| 亚洲天堂福利av| 亚洲精品中文字幕乱码三区不卡| 加勒比一区二区三区在线| 99视频精品在线| 国产一区二区三区无遮挡| aaaa一级片| 国产一区二区在线视频| 国产三级精品网站| 啪啪小视频网站| 久久aⅴ国产紧身牛仔裤| 性视频1819p久久| www.youjizz.com亚洲| 亚洲最新色图| 精品中文字幕在线2019| 一区二区视频免费看| 91精品一区二区三区综合在线爱 | 91精品国产91久久久久久最新毛片| www日韩视频| 日韩精品专区| 色婷婷av一区二区三区软件| 草草草在线视频| 日本精品不卡| 在线观看日韩av先锋影音电影院| 欧美一级片中文字幕| 日韩在线短视频| 欧美亚洲禁片免费| www.com久久久| 一区二区三区国产好| 精品国一区二区三区| 手机免费看av片| 日韩免费电影在线观看| 亚洲人成毛片在线播放| 污污视频网站在线免费观看| 99国产精品一区二区| 欧美成人在线网站| 日韩精品无码一区二区| 久久久青草婷婷精品综合日韩| 国产成人福利视频| 91九色蝌蚪91por成人| 美国毛片一区二区| 亚洲一区免费网站| 日韩一区二区三区在线观看视频| 91在线观看高清| 青青成人在线| 超碰在线无需免费| 亚洲国产成人精品视频| 精品久久久久久久无码| **日韩最新| 欧美精品一区二区三区四区 | 久久久视频在线| 亚洲欧美自拍视频| 美女国产一区二区三区| 草莓视频一区| 国产精品一区二区婷婷| 亚洲人吸女人奶水| 欧美网站免费观看| 日韩一区中文| 日韩电影中文字幕| 开心激情五月网| 一区免费在线| 国产精品视频白浆免费视频| www夜片内射视频日韩精品成人| 91亚洲国产成人精品一区二三| 视频一区二区综合| 男女视频在线| 91久久国产综合久久| 野花视频免费在线观看| 精品成av人一区二区三区| 中文字幕日韩欧美| 人人干人人干人人干| 激情图区综合网| 欧美男人的天堂| 日本一级理论片在线大全| 欧美中文字幕一区二区三区| 欧美性生交xxxxx| 国产精品成人av| 日韩av高清不卡| 蜜桃av中文字幕| 亚洲天堂精品在线观看| 五月婷婷激情久久| 亚洲成a人片77777在线播放| 欧美寡妇偷汉性猛交| 在线观看免费高清视频| 2021国产精品久久精品| 日韩中文字幕在线免费| 久久99成人| www.欧美三级电影.com| 国产精品久久久久久久久久精爆| 丰满白嫩尤物一区二区| 国产四区在线观看| 国产精品黄色片| 亚洲全黄一级网站| 久草视频在线观| 成人av免费在线观看| 粉嫩av一区二区三区天美传媒| 78精品国产综合久久香蕉| 亚洲片av在线| 一级片在线观看免费| 99久久综合狠狠综合久久| www插插插无码免费视频网站| 999精品视频在线观看| 最新中文字幕亚洲| 无码久久精品国产亚洲av影片| 91美女精品福利| 成人在线免费在线观看| 欧美理伦片在线播放| 午夜精品免费视频| 污污视频在线观看网站| 午夜精品免费在线| 国产人妻黑人一区二区三区| 尤物精品在线| 国产一区在线免费| 九九色在线视频| 精品国产污污免费网站入口 | 狠狠入ady亚洲精品| www.av一区视频| 日本片在线观看| 欧美精品一区男女天堂| 日韩 国产 在线| 久久影音资源网| 国产福利视频在线播放| 清纯唯美综合亚洲| 国产精品自产拍高潮在线观看| 一区二区高清不卡| 欧美高清www午色夜在线视频| 外国一级黄色片| 成人爱爱电影网址| www.com毛片| 精品免费av| 91久久精品视频| 久久99亚洲网美利坚合众国| 亚洲国产精品久久久| 欧美在线观看不卡| 国产精品免费免费| 韩国欧美国产1区| 国产欧美一区二区三区久久人妖 | 美女扒开大腿让男人桶| 牛牛视频精品一区二区不卡| 欧美精品导航| 38少妇精品导航| 国产69久久| 欧美浪妇xxxx高跟鞋交| 国产亚洲精久久久久久无码77777| 成人免费视频app| 欧美精品色婷婷五月综合| 成人激情电影在线| 国产精品一区二区三区久久久| 亚洲va综合va国产va中文| 99精品在线观看| 黄色91av| 亚洲天堂手机| 日韩电影在线观看中文字幕| 一级淫片免费看| 亚洲高清不卡在线观看| 妺妺窝人体色WWW精品| 国产在线视频一区二区三区| 亚洲熟妇av日韩熟妇在线| 日韩大片在线观看| 国产精品久久亚洲7777| 欧美成人精品三级网站| 欧美激情videoshd| 国产精品免费播放| 欧美大片拔萝卜| 精品国产一区二区三区四| 亚洲天堂免费在线观看视频| 黑人巨大精品欧美| 国产成人免费在线观看不卡| 丰满少妇在线观看| 日韩一区二区久久| 黄色网络在线观看| 不卡在线一区二区| 国产私拍一区| 国产精品99久久免费| 国产成人一区三区| a国产在线视频| 久久成人人人人精品欧| 国产综合视频一区二区三区免费| 日韩精品中文字幕在线一区| 亚洲国产精品无码久久久| 亚洲成人资源在线| 久久久久亚洲AV成人| 欧美激情综合在线| 精品人妻一区二区三区香蕉 | 欧美日韩在线免费观看| 老妇女50岁三级| 中文在线资源观看网站视频免费不卡 | 日干夜干天天干| 亚洲黄色片在线观看| 青青青视频在线免费观看| 91小视频免费观看| 日本69式三人交| 懂色av噜噜一区二区三区av| 污污的视频免费观看| 蜜桃在线一区二区三区| 黄色一级二级三级| 快she精品国产999| 欧美日韩激情视频在线观看| 在线观看不卡| 黄色国产一级视频| 99视频一区| 日韩激情免费视频| 在线亚洲观看| 成年人视频网站免费观看| 亚洲精选在线| www.中文字幕在线| 99国内精品| 女人和拘做爰正片视频| 日韩视频在线一区二区三区 | 四虎一区二区| sdde在线播放一区二区| 日本一区二区免费看| 欧美日韩伦理在线免费| 丝袜足脚交91精品| 91精品一区国产高清在线gif| 99亚洲精品视频| 女人天堂亚洲aⅴ在线观看| 国产一二三四区在线观看| 欧美在线亚洲综合一区| 国产一二三在线视频| 99精品国产福利在线观看免费 | 亚洲精品一区二区三区福利| 天天摸天天碰天天爽天天弄| 亚洲激情成人网| 飘雪影院手机免费高清版在线观看| 精品一区二区三区三区| 黄色免费在线播放| 最新91在线视频| 中文字幕在线观看播放| 欧美国产精品日韩| 免费高潮视频95在线观看网站| 欧美最猛黑人xxxx黑人猛叫黄| 精品123区| 91成人免费看| 思热99re视热频这里只精品| 日本高清视频一区二区三区| 欧美日韩在线观看视频小说| 一区二区三区不卡在线| 欧美视频网站| 欧美极品欧美精品欧美图片| 久久国产剧场电影| 日韩精品国产一区| 久久久午夜精品理论片中文字幕| 99久久99久久精品免费看小说.| 综合激情成人伊人| 日韩男人的天堂| 欧美视频在线不卡| 亚洲国产精彩视频| 亚洲深夜福利在线| 中文av资源在线| 日本韩国在线不卡| 欧美日韩午夜电影网| 欧美大陆一区二区| 天天操综合网| 亚洲欧洲日产国码无码久久99| 免费av网站大全久久| 久久久久无码国产精品一区李宗瑞| 久久亚洲春色中文字幕久久久| 久草手机视频在线观看| 欧美午夜影院在线视频| 99视频国产精品免费观看a| 亚洲人成在线电影| 欧美1—12sexvideos| 国产精品久久久久久一区二区 | 亚洲午夜激情免费视频| 亚洲大胆人体大胆做受1| 国产精品www| 国产精品欧美大片| 一区二区在线高清视频| 亚洲欧美激情诱惑| 欧美国产在线一区| 亚洲国产精品激情在线观看| 日本在线观看中文字幕| 91精品黄色片免费大全| 第九色区av在线| 97超碰国产精品女人人人爽| 欧一区二区三区| 亚洲午夜久久久影院伊人| 国产日韩专区| 免费黄色三级网站| 亚洲一区在线电影| 国产精品嫩草影院精东| 在线播放日韩精品| 色戒汤唯在线| 国产偷国产偷亚洲高清97cao| 亚洲精品91| 日韩中文字幕a| 久久精品亚洲乱码伦伦中文 | 麻豆一区区三区四区产品精品蜜桃| 欧美日韩专区| 国产一级免费大片| 国产精品久久久久天堂| 天天干,天天干| 亚洲国模精品一区| 久草在线视频福利| 99久久无色码| 欧美涩涩视频| 中文字幕在线播放一区二区| 亚洲婷婷国产精品电影人久久| 在线观看色网站| 最近2019免费中文字幕视频三| 日本一区二区三区视频在线| 日韩免费av电影| 日韩精品一区第一页| 国产成人无码精品久久二区三| 一本一道综合狠狠老| 风间由美一区| 国产精品视频久久久久| 日韩精品不卡一区二区| 亚洲老女人av| 日韩一区在线看| 99热这里精品| 欧美高清自拍一区| 欧美调教视频| 国产日韩一区二区在线观看| 久久精品网站免费观看| 波多野结衣黄色| 最近2019中文字幕第三页视频 | 毛片在线网址| 国产精选一区二区| 国产女优一区| 丰满的亚洲女人毛茸茸| 欧美美女视频在线观看| 在线看三级电影| 国产日韩欧美综合精品| 久久激情久久| 亚洲黄色网址大全| 91精品国产综合久久久久久| 秋霞在线午夜| 蜜桃导航-精品导航| 日韩精品高清不卡| 国产精品精品软件男同| 欧美成人综合网站| 神马久久午夜| 夜夜爽www精品| 国产成人综合在线观看| 国产精品乱子伦| 中文字幕国产精品| 亚洲天堂av资源在线观看| 91九色在线观看视频| 国产精品午夜久久| 亚洲第一大网站| 欧美中文在线免费| 四季av在线一区二区三区| 国内精品免费视频| 91黄色免费观看| av观看在线| 欧美日韩最好看的视频| 黄色精品一二区| 日韩 欧美 中文| 久久久999国产精品| 欧美美女啪啪| 婷婷中文字幕在线观看| 懂色av中文一区二区三区天美| 888av在线| 国产在线观看一区| 精品影视av免费| 亚洲永久精品在线观看| 久久精品国产久精国产一老狼| 欧美一区二区三区红桃小说| www.精品在线| 精品免费在线视频| av中文字幕在线观看| 日日夜夜精品网站| zzijzzij亚洲日本少妇熟睡|