2019年6月8日 星期六

JavaScript: String: codePointAt() 不是字元索引,而是陣列索引

JavaScript 中,要如何拿到字串中的第 i 個字元?用 charCodeAt( i ) 嗎?錯。用codePointAt( i ) 嗎?還是錯。

字元索引 vs 陣列索引


JavaScript 字串的本質 (內部表示) 是 UTF-16 編碼的陣列,不論是 charCodeAt() 或是 codePointAt(),它們的參數都是對該陣列的索引值。

這兩個函式的差異,在於 charCodeAt() 不會處理 UTF-16 編碼中的代理對 (surrogate pair) ,而 codePointAt() 則會在遇到代理對時,把指定陣列位置與下個位置的 code unit (各 16 bits) ,一起合併起來成為 code point (函式名稱的由來),才傳回來。

但無論如何,它們的參數,都不是字元索引值,而是陣列索引值。

String's codePointAt()


換句話說,codePointAt( i ) 並不是傳回第 i 個字元,而是傳回 (把該字串以 UTF-16 編碼時) 陣列索引位置 i 開始的一個字元。換句話說,如果位置 i 的字元是佔用兩的 16 bits 的字元,那麼 codePointAt( i ) 確實可以拿到完整的該字元,但是 codePointAt( i + 1 ) 並不是拿到下一個字元,而是拿到該字元的後半部。

String as Iterator


要正確依序拜訪字串中的每個字元 (code point),把字串視為 iterator 來處理會比較容易正確。也就是用 for-of 或 spread operator。

例如:字串 s = "𥑮b" 中,要如何拿到 “b” 這個字元?

let s = "𥑮b";

是 s.codePointAt( 1 ) 嗎?錯。正確答案是 s.codePointAt( 2 )。這是因為 "𥑮" 是由兩個 code unit (兩個 16 bits,共 32 bits) 組成的代理對 (surrogate pair)。用 s.codePointAt( 0 ) 固然能夠順利拿到 "𥑮" 這個字元的完整字元碼 (code point),但 s.codePointAt( 1 ) 卻會拿到後半個 (不完整的) "𥑮" 字,並不會拿到  "b"。

但如果用 for-of 去拜訪字串中的每個字元:

for (let c of s) {

}

或者用 spread operator 把每個字元解開放到陣列中:

let arr = [ ...s ];

都可以順利得到正確的兩個字元 [ "𥑮", "b" ]。