Unicode in JavaScript

照我的理解,Unicode为每一个字符指定了一个数字,称为code point。

const cp = 'a'.codePointAt(0)
console.log(cp)
console.log(String.fromCodePoint(cp))
// cp的16进制
console.log('\x61')

JavaScript内部使用UTF-16,每个code point用一到两个16bit的code unit编码。BMP(0–65535)里的code point用一个code unit表示,BMP之外的code point用两个code unit表示,称为surrogate pair。BMP包含绝大部分常用的字符,包括汉字。

const cp = '我'.codePointAt(0)
console.log(cp)
console.log(String.fromCodePoint(cp))

而emoji则位于BMP之外。许多JavaScript的原生方法作用于code unit,处理emoji时,会产生一些奇怪的结果。

const cp = '😀'.codePointAt(0)
console.log(cp)
console.log(String.fromCodePoint(cp))
console.log('😀'.length)
console.log('😀'.split(''))
console.log('😀'[0])

显然lengthsplit只能处理code unit,😀由两个code unit表示,因此长度为2。Surrogate pair里的code unit单独存在时没有任何意义。一些生僻字同样在BMP之外。

const cp = '𬊈'.codePointAt(0)
console.log(cp)
console.log(String.fromCodePoint(cp))
console.log('𬊈'.length)

codePointAtString.fromCodePoint作用于code point,以下一些方法也能处理code point。

console.log(Array.from('😀'))
console.log([...'😀'])

for (const a of '😀') {
  console.log(a)
}

显然,以index来循环,だめ。

const a = '😀'

for (let i = 0; i < a.length; i++) {
  console.log(a[i])
}

另有一些被称为grapheme cluster的字符,这些字符看起来、我们会把它们理解为一个字符,但实际上由多个code point构成。例如🖖🏻即由两个code point构成。

const s = "🖖🏻"
console.log(s.length)
console.log([...s])
console.log(String.fromCodePoint(s.codePointAt(0), s.codePointAt(2)))

目前JavaScript没有原生的方法可以处理grapheme cluster。


A not so interesting tidbit

在Chrome的控制台里输入🖖🏻,需要按两次删除键才能删掉,第一下只会删掉第二个code point,留下了🖖。