关于js汉字编码的问题

问题出现
在项目中遇到问题
“一二三四五?六七八九十”,被识别为10个字。
一、二、...、十 。呃...似乎有点问题。应该是11个字的啊。
问题就出在?,这个字没有被识别。

问题分析
看看代码:

JavaScript允许直接用码点表示Unicode字符,写法是"反斜杠+u+码点"。码点有十六机制数表示。
但是,这种表示法对4字节的码点无效。ES6修正了这个问题,只要将码点放在大括号内,就能正确识别。

根据汉字unicode范围表发现常用的大多数汉字都可用u4E00-u9FA5来表示。而?的unicode码是U+20BB7,没有被包含。所以,我们需要匹配汉字unicode范围表所有的汉字。

但是, 编码类似u20BB7的4字节的码点不能直接被识别。我们需要理解一下js使用的编码。
JavaScript使用哪一种编码?
JavaScript用的是UCS-2!

UCS的开发进度快于Unicode,1990年就公布了第一套编码方法UCS-2,使用2个字节表示已经有码点的字符。(那个时候只有一个平面,就是基本平面,所以2个字节就够用了。)UTF-16编码迟至1996年7月才公布,明确宣布是UCS-2的超集,即基本平面字符沿用UCS-2编码,辅助平面字符定义了4个字节的表示方法。

由于JavaScript只能处理UCS-2编码,造成所有字符在这门语言中都是2个字节,如果是4个字节的字符,会当作两个双字节的字符处理。JavaScript的字符函数都受到这一点的影响,无法返回正确结果。
unicode

这么多符号,Unicode不是一次性定义的,而是分区定义。每个区可以存放65536个(216)字符,称为一个平面(plane)。目前,一共有17个(25)平面,也就是说,整个Unicode字符集的大小现在是221。
最前面的65536个字符位,称为基本平面(缩写BMP),它的码点范围是从0一直到216-1,写成16进制就是从U+0000到U+FFFF。所有最常见的字符都放在这个平面,这是Unicode最先定义和公布的一个平面。
剩下的字符都放在辅助平面(缩写SMP),码点范围从U+010000一直到U+10FFFF。

UTF-16

到底是把这两个字节当作一个字符还是与后面的两个字节一起当作一个字符呢? 这里有一个很巧妙的地方,在基本平面内,从 U+D800 到
U+DFFF 是一个空段,即这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。 辅助平面的字符位共有 220220
个,因此表示这些字符至少需要 20 个二进制位。UTF-16将这 20 个二进制位分成两半,前 10 位映射在 U+D800 到
U+DBFF,称为高位(H),后 10 位映射在 U+DC00 到
U+DFFF,称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。 因此,当我们遇到两个字节,发现它的码点在
U+D800 到 U+DBFF 之间,就可以断定,紧跟在后面的两个字节的码点,应该在 U+DC00 到 U+DFFF
之间,这四个字节必须放在一起解读。

总的来说,一个辅助平面的字符,被拆成两个基本平面的字符表示。或者在ES6中可以用'u{20BB7}'来表示。

ES6的支持

  • ES6可以自动识别4字节的码点。因此,遍历字符串就简单多了。

    for (let s of string ) { // ...
    }

为了得到字符串的正确长度,可以用下面的方式。

Array.from(string).length   

[...string].length
  • 码点表示法,JavaScript允许直接用码点表示Unicode字符,写法是"反斜杠+u+码点"。但是,这种表示法对4字节的码点无效。ES6修正了这个问题,只要将码点放在大括号内,就能正确识别。
  • String.fromCodePoint():从Unicode码点返回对应字符
  • String.prototype.codePointAt():从字符返回对应的码点
  • String.prototype.at():返回字符串给定位置的字符
  • 正则表达式,ES6提供了u修饰符,对正则表达式添加4字节码点的支持。

问题解决
所以,我们在正则中表示所有的汉字,需要将不能被直接识别的4字节识别,可以通过ES6的方式,也可以转换成基本平面来表示。我们需要将所有的汉字区间都包含在匹配公式中,从上面的汉字unicode范围表看,自己来手写是很复杂。
还好,有Regenerate可以来完成这件事情。通过它我们可以快速的表示出复杂的正则表达式。

export const character2unicode = regenerate()
  .addRange(0x4e00, 0x9fa5)
  .addRange(0x9fa6, 0x9fcb)
  .addRange(0x3400, 0x4db5)
  .addRange(0x20000, 0x2a6d6)
  .addRange(0x2a700, 0x2b734)
  .addRange(0x2b740, 0x2b81d)
  .addRange(0x2f00, 0x2fd5)
  .addRange(0x2e80, 0x2ef3)
  .addRange(0xf900, 0xfad9)
  .addRange(0x2f800, 0x2fa1d)
  .addRange(0xe815, 0xe86f)
  .addRange(0xe400, 0xe5e8)
  .addRange(0xe600, 0xe6cf)
  .addRange(0x31c0, 0x31e3)
  .addRange(0x2ff0, 0x2ffb)
  .addRange(0x3105, 0x3120)
  .addRange(0x31a0, 0x31ba)
  .toRegExp();

// character2unicode 
// /[\u2E80-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3105-\u3120\u31A0-\u31BA\u31C0-\u31E3\u3400-\u4DB5\u4E00-\u9FCB\uE400-\uE5E8\uE600-\uE6CF\uE815-\uE86F\uF900-\uFAD9]|[\uD840-\uD868\uD86A-\uD86C][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D]|\uD87E[\uDC00-\uDE1D]/

以上就可以表示出汉字unicode范围编码的正则。感觉可以很愉快的开工。

参考
https://mathiasbynens.be/note...
http://www.ruanyifeng.com/blo...
https://github.com/mathiasbyn...