來源:juejin.cn/post/7025400771982131236
在開發程序中偶爾會遇到關于編碼、Unicode,Emoji 的問題,發現自己對這方面的基礎知識并沒有充分掌握,所以在經過一番查找學習之后,整理幾篇通俗易懂的文章分享出來,
不知道你是否遇到過這樣的疑惑,在做表單校驗長度的需求中,發現不同字符 length 可能大小不一,比如標題中的 "??" length 是 2(需要注意??,這并不是一個中文字!),
'吉'.length
// 1
'??'.length
// 2
'?'.length
// 1
'??'.length
// 2
復制代碼
要解釋這個問題要從 UTF-16 編碼說起,
UTF-16
從 ECMAScript? 2015 規范中可以看到,ECMAScript 字串使用的是 UTF-16 編碼,
定與不定: UTF-16 最小的碼元是兩個位元組,即使第一個位元組可能都是 0 也要占位,這是固定的,不定是對于基本平面(BMP)的字符只需要兩個位元組,表示范圍
U+0000 ~ U+FFFF,而對于補充平面則需要占用四個位元組U+010000~U+10FFFF,
在上一篇文章中,我們有介紹過 utf-8 的編碼細節,了解到 utf-8 編碼需要占用 1~4 個位元組不等,而使用 utf-16 則需要占用 2 或 4 個位元組,來看看 utf-16 是怎么編碼的,
UTF-16 的編碼邏輯
UTF-16 編碼很簡單,對于給定一個 Unicode 碼點 cp(CodePoint 也就是這個字符在 Unicode 中的唯一編號):
- 如果碼點小于等于
U+FFFF(也就是基本平面的所有字符),不需要處理,直接使用, - 否則,將拆分為兩個部分
((cp – 65536) / 1024) + 0xD800,((cp – 65536) % 1024) + 0xDC00來存盤,
Unicode 標準規定 U+D800...U+DFFF 的值不對應于任何字符,所以可以用來做標記,
舉個具體的例子:字符 A 的碼點是 U+0041,可以直接用一個碼元表示,
'\u0041'
// -> A
A === '\u0041'
// -> true
復制代碼
Javascript 中 \u 表示 Unicode 的轉義字符,后面跟著一個十六進制數,
而字符 ?? 的碼點是 U+1f4a9,處于補充平面的字符,經過 ?? 公式計算得到兩個碼元 55357, 56489 這兩個數字用十六進制表示為 d83d, dca9,將這兩個編碼結果組合成代理對,
'\ud83d\udca9'
// -> '??'
'??' === '\ud83d\udca9'
// -> true
復制代碼
由于 Javascript 字串使用 utf-16 編碼,所以可以正確將代理對 \ud83d\udca9 解碼得到碼點 U+1f4a9,
還可以使用 \u + {},大括號中直接跟碼點來表示字符,看起來長得不一樣,但他們表示的結果是一樣的,
'\u0041' === '\u{41}'
// -> true
'\ud83d\udca9' === '\u{1f4a9}'
// -> true
復制代碼
可以打開 Dev Tool 的 console 面板,運行代碼驗證結果,
所以為什么 length 判斷會有問題?
要解答這個問題,可以繼續查看規范,里面提到:在 ECMAScript 操作解釋字串值的地方,每個元素都被解釋為單個 UTF-16 代碼單元,
Where ECMAScript operations interpret String values, each element is interpreted as a single UTF-16 code unit.
所以像?? 字符實際上占用了兩個 UTF-16 的碼元,也就是兩個元素,所以它的 length 屬性就是 2,(這跟一開始 JS 使用 USC-2 編碼有關,當初以為 65536 個字符就可以滿足所有需求了)
但對于普通用戶而言,這就完全沒辦法理解了,為什么明明只填了一個 '??',程式上卻提示占用了兩個字符長度,要怎樣才能正確識別出 Unicode 字符長度呢?
我在 Antd Form 表單使用的 async-validator 包中可以看到下面這段代碼
const spRegexp = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
if (str) {
val = value.replace(spRegexp, '_').length;
}
復制代碼
當需要進行字串長度的判斷時,會將碼點范圍在補充平面的字符全部替換為下劃線,這樣長度判斷就和實際顯示的一致了!!!
ES6 對 Unicode 的支持
length 屬性的問題,主要還是最初設計 JS 這門語言的時候,沒有考慮到會有這么多字符,認為兩個位元組就完全可以滿足,所以不止是 length,字串常見的一些操作在 Unicode 支持上也會表現例外,
下面的內容將介紹部分存在例外的 API 以及在 ES6 中如何正確處理這些問題,
for vs for of
例如使用 for 回圈列印字串,字串會按照 JS 理解的每個“元素”遍歷,輔助平面的字符將會被識別成兩個“元素”,于是出現“亂碼”,
var str = '??yo??'
for (var i = 0; i < str.length; i ++) {
console.log(str[i])
}
// -> ?
// -> ?
// -> y
// -> o
// -> ?
// -> ?
復制代碼
而使用 ES6 的 for of 語法就不會,
var str = '??yo??'
for (const char of str) {
console.log(char)
}
// -> ??
// -> y
// -> o
// -> ??
復制代碼
展開語法(Spread syntax)
前面提到了使用正則運算式,將輔助平面的字符替換的方式來統計字符長度,使用展開語法也可以得到同樣的效果,
[...'??'].length
// -> 1
復制代碼
slice, split, substr 等等方法也存在同樣的問題,
正則運算式 u
ES6 中還針對 Unicode 字符增加了 u 描述符,
/^.$/.test('??')
// -> false
/^.$/u.test('??')
// -> true
復制代碼
charCodeAt/codePointAt
對于字串,我們還常用 charCodeAt 來獲取 Code Point,對于 BMP 平面的字符是可以適用的,但是如果字符是輔助平面字符 charCodeAt 回傳結果就只會是編碼后第一個碼元對于的數字,
'羽'.charCodeAt(0)
// -> 32701
'羽'.codePointAt(0)
// -> 32701
'??'.charCodeAt(0)
// -> 55357
'??'.codePointAt(0)
// -> 128568
復制代碼
而使用 codePointAt 則可以將字符正確識別,并回傳正確的碼點,
String.prototype.normalize()
由于 JS 中將字串理解成一串兩個位元組的碼元序列,判斷是否相等是根據序列的值來判斷的,所以可能存在一些字串看起來長得一模一樣,但是字串相等判斷結果確是 false,
'cafe?' === 'café'
// -> false
復制代碼
上面代碼中第一個 cafe? 是有 cafe 加上一個縮進的音標字符\u0301組成的,而第二個 café 則是由一個 caf + é 字符組成的,所以兩者雖然看上去一樣,但碼點不一樣,所以 JS 相等判斷結果為 false,
'cafe\u0301'
// -> 'cafe?'
'cafe\u0301'.length
// -> 5
'café'.length
// -> 4
復制代碼
為了能正確識別這種碼點不一樣,但是語意一樣的字串判斷,ES6 增加了 String.prototype.normalize 方法,
'cafe\u0301'.normalize() === 'café'.normalize()
// -> true
'cafe\u0301'.normalize().length
// -> 4
復制代碼
總結
這篇文章主要是我最近重新學習編碼的學習筆記,由于時間倉促 && 水平有限,文章中必定存在大量不準確的描述、甚至錯誤的內容,如有發現還請善意指出,??
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
2.勁爆!Java 協程要來了,,,
3.Spring Boot 2.x 教程,太全了!
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/500245.html
標籤:其他
上一篇:Druid 查詢超時配置的探究 → DataSource 和 JdbcTemplate 的 queryTimeout 到底誰生效?
下一篇:jvm類加載程序簡介說明
