1. 预备知识
码位
:code point / position,组成代码页的数值。例如:ASCII码包含128个码位,范围是0到7F。码元
:code unit,指一个已编码的文本中具有最短的 位(bit)组合 的单元。例如:UTF-16的码元是16bit长。UTF-16
:是Unicode字符编码表的一种实现方式。即把Unicode字符集的抽象 码位 映射为16bit长的整数(码元)的序列。Unicode的码位需要1~2个16bit长的码元表示。代理对(surrogate pair)
:超出1个16位码元表示范围的码位(辅助平面的码位)需要2个码元表示,则称组成该码位的两个码元组合为 代理对 ,分别为前导代理(high-surrogate)码元和后尾代理(low-surrogate)码元。-
Unicode
:与ISO(通用字符集)类似的字符集。对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字。- Unicode编码范围从U+0000到U+10FFFF,共计2^20个,采用两个16位长的整数组成。
- Unicode编码分为17个平面(plane),每个平面包含65536个码位。
- 每个平面的码位可表示为U+xx0000 ~ U+xxFFFF。
- 第一个平面为基本多语言平面(Basic Multilingual Plane, BMP),或第零平面。
- BMP内,从U+D800~U+DFFF(8 x 16^2共计2048个值)之间的码位是永久保留不映射到Unicode字符。
- 代理对的高位代理表示前10位+0xD800,低位代理表示后10位+0xDC00。
组合字符
:组合字符是由两个码位组成的字符,由基础字符结合特殊字符组成。注意组合字符并不是1个代理对,而是由两个码位组成。例:
console.log('\u0061'); // => 'a'
,console.log('\u030A'); // => "̊ "
console.log('\u0061\u030A'); // => 'å'
2. JavaScript中的Unicode
String定义:String类型是由0个或多个16位无符号整型值(元素)组成的有序序列,最大长度为2^53-1个元素。String的每个元素被当做一个UTF-16的码元。
- 字符串值作为UTF16编码的Unicode码位表示:
- BMP平面内,0 ~ 0xD7FF 和 0xE000 ~ 0xFFFF范围内的码元解释为等值得码位;
- 有两个码元的序列,若第一个码元(c1)为0xD800 ~ 0xDBFF(2^10=1024前导代理),且第二个码元(c2)为0xDC00 ~ 0xDFFF(2^10=1024后尾代理)。则该序列为一个代理对,解释为辅助平面的码位。
- 值为0xD800 ~ 0xDFFF但是不是代理对的码元,解释为等值的码位。
-
问题
当JS操作字符串值时,每个元素被当做单个的UTF-16码元。然而JS对字符串值中的码元序列并不做限制,因此字符串作为UTF16码元序列解释时可能会出现不正确的格式。操作符将字符串内容作为无区分的16位整型值序列。 -
例:
let letter = 'e\u0301'; console.log(letter); // => 'é' console.log(letter.length); // => 2
如果字符串中含有代理对或者组合字符,则一些字符串操作会带来困扰:
2.1 字符串比较:
一些带有语调符号的字符Unicode提供两种表示方式,一种直接提供对应的码位(不一定是代理对,也有可能在BMP平面),一种提供组合字符(即原字符与语调符号的组合)。因此,字符串的渲染结果并不能明确地反应它的码元,相同渲染结果的字符串长度也可能不同。
-
例:
var str1 = 'ça va bien'; var str2 = 'c\u0327a va bien'; console.log(str1); // => 'ça va bien' console.log(str2); // => 'ça va bien' console.log(str1.length); // => 10 console.log(str2.length); // => 11 console.log(str1 === str2); // => false
字素
ç
有两种组成方式:- 使用
U+00E7
LATIN SMALL LETTER C WITH CEDILLA; - 组合字符序列:
U+0063
LATIN SMALL LETTER C 加上U+0327
COMBINING CEDILLA.
示例中的两种表示方法,在视觉和语义上都等价,但是js不能将不同的码元序列作为为等价字符串。
- 使用
-
解决方法:字符串标准化(Normalization)。
标准化(Normalization):是指将字符串转换为统一的等价表示形式,以保证具有标准等价性( canonical-equivalent)(或兼容等价性( compatibility-equivalent))的字符串只有一种表示形式。
- ES2015提供
myString.normalize([normForm])
方法标准化方法,normForm
是一个可选参数(默认为NFC
),取值为以下标准化模式之一:- NFC,默认参数,“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的组合字符。
- NFD,“标准等价分解”(Normalization Form Canonical Decomposition),返回组合字符分解的多个简单字符。
- NFKC,“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价。
- NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。
- ES2015提供
2.2 字符串长度:
string的length属性只是字符串的码元个数,如果字符串中含有代理对或组合字符,则length属性的值会比预期的字符串长度大。
- 代理对
ES2015提供一种能识别代理对的方法:字符迭代器String.prototype[@@iterator]()
注意:使用字符迭代器会影响性能。var str = 'cat\u{1F639}'; console.log(str.length); // => 5 console.log([...str].length); // => 4 console.log(Array.from(str).length); // => 4
why?
- 组合字符
组合字符可使用标准化后再计算长度:
注意:标准化并不能处理所有的组合字符问题。一些组合字符序列并不都有对应的单个字符标准形式。var drink = 'cafe\u0301'; console.log(drink); // => 'café' console.log(drink.length); // => 5 console.log(drink.normalize()) // => 'café' console.log(drink.normalize().length); // => 4
-
例:
var drink = 'cafe\u0327\u0301'; console.log(drink); // => 'cafȩ́' console.log(drink.length); // => 6 console.log(drink.normalize()); // => 'cafȩ́' console.log(drink.normalize().length); // => 5
-
例:
2.3 字符定位:
字符串是码元序列,通过字符串索引定位双码元字符有困难。
-
代理对:
如果使用字符串索引访问代理对,只能返回一个高位代理或低位代理,为无效的不可打印字符。
访问代理对字符有以下两种方法:- 使用能够识别Unicode的字符串迭代器生成一个字符数组[...str][index]
-
推荐方法:用
number = myString.codePointAt(index)
获取码位,然后用String.fromCodePoint(number)
将码位转换为字符。
var omega = '\u{1D6C0} is omega'; console.log(omega); // => '𝛀 is omega' // Option 1 console.log([...omega][0]); // => '𝛀' // Option 2 var number = omega.codePointAt(0); console.log(number.toString(16)); // => '1d6c0' console.log(String.fromCodePoint(number)); // => '𝛀'
-
组合字符:标准化后访问。(使用NFC,将组合字符合成为单个)
var drink = 'cafe\u0301'; console.log(drink.normalize()); // => 'café' console.log(drink.normalize().length); // => 4 console.log(drink.normalize()[3]); // => 'é'
注意:与字符串长度类似,标准化并不能解决所有组合字符定位,因为并非所有组合字符都有对应的单个标准字符。
2.4 正则匹配:
正则表达式与字符串一样,也是基于码元的。因此,使用正则表达式在处理代理对和组合字符序列时也会遇到困难。
var regex = /[😀-😎]/;
// Uncaught SyntaxError: Invalid regular expression: /[😀-😎]/: Range out of order in character class at <anonymous>:1:13
示例中的正则表达式表示匹配字符😀与字符😎范围之间的字符,然而辅助平面字符用代理对表示,因此regex
被js表示为/[\uD83D\uDE00-\uD83D\uDE0E]/
。然而在正则表达式中,每个码元被当做一个单独的元素,代理对被忽略。\uDE00
大于\uD83D
,\uDE00-\uD83D
这个字符区间是无效的。
解决方法:使用正则表达式u
标识。
在正则表达式中可以使用Unicode转义序列/u{1F600}/u
。
var x = "\uD83D\uDE00" // x = 😀
var regex = /\u{1F600}/u;
regex.test(x) // true
注意:不论有没有u标志,正则表达式都会把组合字符视为独立的码元来处理。
2.5 JS转义序列:
- 16进制转义序列:是最短的转义序列,\x<hex>,<hex>是一个2位的16进制数。
- Unicode转义序列:可表示整个BMP的码位,两个连续的Unicode转义序列也可表示代理对组成的辅助平面的码位,\u<hex>,<hex>是一个4位的16进制数。
- 码位转义序列:ES6新提供的代表整个Unicode空间的码位,即BMP和辅助平面。表示方法:\u{<hex>},<hex>是1~6位的16进制数。可替代Unicode转义的代理对使用。
- 8进制转义序列:<8进制>,ES v3不支持,谨慎使用。
参考文章