字符编码

1. 预备知识

  1. 码位:code point / position,组成代码页的数值。例如:ASCII码包含128个码位,范围是0到7F。

  2. 码元:code unit,指一个已编码的文本中具有最短的 位(bit)组合 的单元。例如:UTF-16的码元是16bit长。

  3. UTF-16:是Unicode字符编码表的一种实现方式。即把Unicode字符集的抽象 码位 映射为16bit长的整数(码元)的序列。Unicode的码位需要1~2个16bit长的码元表示。

  4. 代理对(surrogate pair):超出1个16位码元表示范围的码位(辅助平面的码位)需要2个码元表示,则称组成该码位的两个码元组合为 代理对 ,分别为前导代理(high-surrogate)码元和后尾代理(low-surrogate)码元。

  5. 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。
  6. 组合字符:组合字符是由两个码位组成的字符,由基础字符结合特殊字符组成。注意组合字符并不是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 
    

    字素 ç 有两种组成方式:

    1. 使用U+00E7LATIN SMALL LETTER C WITH CEDILLA;
    2. 组合字符序列:U+0063 LATIN SMALL LETTER C 加上U+0327 COMBINING CEDILLA.

    示例中的两种表示方法,在视觉和语义上都等价,但是js不能将不同的码元序列作为为等价字符串。

  • 解决方法:字符串标准化(Normalization)。

    标准化(Normalization):是指将字符串转换为统一的等价表示形式,以保证具有标准等价性( canonical-equivalent)(或兼容等价性( compatibility-equivalent))的字符串只有一种表示形式。

    • ES2015提供myString.normalize([normForm])方法标准化方法,normForm是一个可选参数(默认为NFC),取值为以下标准化模式之一:
      1. NFC,默认参数,“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的组合字符。
      2. NFD,“标准等价分解”(Normalization Form Canonical Decomposition),返回组合字符分解的多个简单字符。
      3. NFKC,“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价。
      4. NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。
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 字符定位:

字符串是码元序列,通过字符串索引定位双码元字符有困难。

  • 代理对:
    如果使用字符串索引访问代理对,只能返回一个高位代理或低位代理,为无效的不可打印字符。
    访问代理对字符有以下两种方法:

    1. 使用能够识别Unicode的字符串迭代器生成一个字符数组[...str][index]
    2. 推荐方法:用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 :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转义序列:
  1. 16进制转义序列:是最短的转义序列,\x是一个2位的16进制数。
  2. Unicode转义序列:可表示整个BMP的码位,两个连续的Unicode转义序列也可表示代理对组成的辅助平面的码位,\u是一个4位的16进制数。
  3. 码位转义序列:ES6新提供的代表整个Unicode空间的码位,即BMP和辅助平面。表示方法:\u{},是1~6位的16进制数。可替代Unicode转义的代理对使用。
  4. 8进制转义序列:<8进制>,ES v3不支持,谨慎使用。

参考文章

  1. 每个JavaScript开发者都该懂的Unicode
  2. What every JavaScript developer should know about Unicode
  3. ES6关于Unicode的相关扩展

你可能感兴趣的:(字符编码)