字符编码

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

参考文章

  1. 每个JavaScript开发者都该懂的Unicode
  2. What every JavaScript developer should know about Unicode
  3. ES6关于Unicode的相关扩展
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容