1. 程式人生 > 其它 >js 傳遞漢字 亂碼_JavaScript 字串反轉亂碼問題解決

js 傳遞漢字 亂碼_JavaScript 字串反轉亂碼問題解決

技術標籤:js 傳遞漢字 亂碼

JavaScript 字串反轉亂碼

前一段寫了篇文章:JavaScript 8種字串反轉的方法 – 碼中人,介紹了js中反轉字串的幾個方法。

但有些字串或有些方法無法達到預期結果,如:

emoji字元

b14b216f0a72354dd5c9ee968f2815e5.png
"Trump ".split("").reverse().join("");// "�� pmurT"
c6645b3086bda1bf2b75f8c5d8457942.png

生僻漢字

5073ebe7c505379d8955402057367d2b.png
"野家".split("").reverse().join("");// "家野��"
204476d711f979de0904f39dc6ec2411.png

複合字符

"noël".split("").reverse().join("");//"l̈eon"
e5a26895365c824779d9829e2617abc1.png

字元逆轉雖然沒有出現亂碼,但原本字母”e”上面修飾符,變成了”l”的修飾符。


產生亂碼的原因

以上三例子的錯誤是在哪個步驟產生的?先來看string.split()之後:

5fc06bdf74753669d476d2da50eb57ca.png 5eeaac073c565b937ce3d6503745a229.png 5ffbf53a928d0e64ab27a1b4f5856816.png

顯然在split()方法就已經出錯了,並且 長度為n的字串,轉化成了n+1長度的陣列。

本質原因要回溯到javascript字元編碼的方式。

Unicode與JavaScript詳解 – 阮一峰的網路日誌

JavaScript語言採用Unicode字符集,最初使用的字元編碼是UCS-2!

但UCS-2 編碼方式只覆蓋基本多語言平面(BMP)的碼點,因為 16 位二進位制表示的最大值為 0xFFFF,而對於增補平面中的碼點(範圍為 0x10000~0x10FFFF,十進位制為 65536~1114111),兩位元組的 16 位二進位制是無法表示的。為了解決這個問題,The Unicode Consortium 提出了通過代理對(surrogate pair)機制來擴充套件原來的 UCS-2 編碼方式,也就是 UTF-16。

可以這麼理解,JavaScript字元編碼是 UCS-2 + 代理對(surrogate pair)。

代理對規定用兩個16位編碼單元來表示一個碼位(超出BMP的碼位),該實現方式相對有些複雜,本文不贅述。

因為代理對,JavaScript字串裡的字元有兩種

  • 由一個碼元(16位)表示的BMP字元
  • 由兩個碼元(16*2位)表示的輔助平面字元

這就給 JS 中的 Unicode 處理帶來了很多問題,基本上所有的字串操作函式在處理非 BMP 字元時都是錯誤的。

“”與“”這兩個字元都是通過代理對錶示的,有2個碼元長度,所以js把它們當成2個字元,長度變成n+1。同時這兩個單獨的碼元所指向的碼點上沒有相應字元,變成了亂碼。


解決方法

1 ES6 自動識別

ES6 中的for of 、array.from(str)、… 都可以自動識別兩個碼元字元,把它當成一個字元。

[... "Trump "].reverse().join("");" pmurT"
74ca19ac81d8364b5a47a6e2a4e4d860.png
Array.from("野家").reverse().join("");"家野"
5940caddf672a5c7e4ca0b47768e8b80.png

2 引入ssrever 類庫

mathiasbynens/esrever: A Unicode-aware string reverser written in JavaScript.

var input = 'Lorem ipsum  dolor sit ameͨ͆t.';var reversed = esrever.reverse(input);console.log(reversed);// → '.teͨ͆ma tis rolod  muspi meroL'esrever.reverse(reversed) == input;// → true

複合字符

但是”noël”的逆轉還是不成功。

Array.from("noël").reverse().join("");"l̈eon"
5e2ae042205564e39bf4317c6d26970c.png

這個問題和 JS 沒關係,是 Unicode 字符集本身的問題。

根據 Unicode 定義,有些字元屬於修飾字符,也就是和別的字元一起出現的時候會修飾別的字元,兩個合在一起構成一個我們人眼中的字元。

比如,ë 這個字元,由兩個 Unicode 碼點構成,分別是 U+0065 和 U+0308。這兩個都是 Unicode 中的合法字元,擁有自己的碼點,但他們合在一起的時候,構成一個我們人類眼中的字元。

同時,在 Unicode 中,還有一個單獨的字元 ë,碼點為 U+00EB。

ë 和 ë 在我們眼中是一樣的字元,但在 Unicode 中卻是不同的表現,一個是由兩個字元拼接而成,另一個是獨立的字元,因此,如果直接比較的話,肯定是不相等的。

"ë" === "ë" // false

這時候就需要引入規整化,將字元轉變為某種特定的形式。Unicode 中定義了四種形式,常用的兩種是:

  1. NFD: Normalization Form Canonical Decomposition,將所有的單個的複合字符轉換為多個字元拼接而成的形式
  2. NFC: Normalization Form Canonical Composition,將所有的拼接而成的符合字元轉換為單個字元的形式

因此,在比較 Unicode 字串之前,我們需要對兩邊的字串規整化到相同的形式,這樣結果才是準確的。ES6 中引入的 String.prototype.normalize 方法可以用於字串的規整化。

"ë".normalize("NFC") === "ë".normalize("NFC") // true
7a01713a04192b864dcb67d46ab5a0da.png

所以,正確反轉“noël”需要先將其規整化。

Array.from("noël".normalize()).reverse().join("");"lëon"
5f65c2227939d8267706ca396804492c.png

參考資料

  • Unicode與JavaScript詳解 – 阮一峰的網路日誌
  • ES6走走看看—字元到底發生了什麼變化 – 掘金
  • Unicode 及編碼方式概述 – IBM Developer