ES6关于Unicode的相关扩展

前面的话

  JS中的字符串类型是由引号括起来的一组由16位Unicode字符组成的字符序列。在过去,16位足以包含任何字符,直到Unicode引入了扩展字符集,编码规则不得不进行变更。本文将详细介绍ES6关于Unicode的相关扩展

概述

  Unicode的目标是为世界上每一个字符提供唯一标识符,唯一标识符称为码位或码点(code point)。而这些码位是用于表示字符的,又称为字符编码(character encode)

  在ES6之前, JS 的字符串以 16 位字符编码(UTF-16)为基础。每个 16 位序列(相当于2个字节)是一个编码单元(code unit),可简称为码元,用于表示一个字符。字符串所有的属性与方法(如length属性与charAt() 方法等)都是基于16位序列

【BMP】

  最常用的Unicode字符使用16位序列编码字符,属于“基本多语种平面”(Basic Multilingual Plane BMP),也称为“零断面”(plan 0), 是Unicode中的一个编码区段,编码介于U+0000——U+FFFF之间。超过这个范围的码位则要归属于某个辅助平面或称为扩展平面(supplementary plane),其中的码位仅用16位就无法表示了

  为此,UTF-16引入了代理对(surrogate pairs),规定用两个16位编码来表示一个码位。这意味着,字符串里的字符有两种:一种由一个码元(共 16 位)来表示BMP字符,另一种用两个码元(共 32 位)来表示辅助平面字符

大括号表示

  JavaScript 允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码位

// "a"
console.log("\u0061");

  但是,这种表示法只限于码位在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示

// "??"
console.log("\uD842\uDFB7");

// "?7"
console.log("\u20BB7");

  上面代码表示,如果直接在\u后面跟上超过0xFFFF的数值(比如\u20BB7),JavaScript会理解成\u20BB+7。所以会显示一个特殊字符,后面跟着一个7

  ES6 对这一点做出了改进,只要将码位放入大括号,就能正确解读该字符

// "??"
console.log("\u{20BB7}");

// "ABC"
console.log("\u{41}\u{42}\u{43}");

let hello = 123;
// 123
console.log(hell\u{6F}); 

// true
console.log(‘\u{1F680}‘ === ‘\uD83D\uDE80‘);

  上面代码中,最后一个例子表明,大括号表示法与四字节的 UTF-16 编码是等价的。

  有了这种表示法之后,JavaScript 共有6种方法可以表示一个字符

‘\z‘ === ‘z‘  // true
‘\172‘ === ‘z‘ // true
‘\x7A‘ === ‘z‘ // true
‘\u007A‘ === ‘z‘ // true
‘\u{7A}‘ === ‘z‘ // true

字符编解码

【codePointAt()】

  ES6新增了完全支持UTF-16的方法codePointAt(),该方法接受编码单元的位置而非字符位置作为参数,返回与字符串中给定位置对应的码位,即一个整数值

var text = "??a" ;

console.log(text.charCodeAt(0)); // 55362
console.log(text.charCodeAt(1)); // 57271
console.log(text.charCodeAt(2)); // 97

console.log(text.codePointAt(0)); // 134071
console.log(text.codePointAt(1)); // 57271
console.log(text.codePointAt(2)); // 97

  对于BMP字符,codePointAt()方法的返回值与 charCodeAt() 相同,如‘a‘,都返回97

  对于辅助平面的32位字符,如‘??‘,charCodeAt()和codePointAt()方法都分为两部分返回

  charCodeAt(0)和chatCodeAt(1)分别返回前16位和后16位的编码;而codePointAt(0)和codePointAt(1)分别返回32位编码及后16位的编码 

  判断一个字符是否是BMP,对该字符调用 codePointAt() 方法就是最简单的方法

function is32Bit(c) {
    return c.codePointAt(0) > 0xFFFF;
}
console.log(is32Bit("??" )); // true
console.log(is32Bit("a")); // false

  16位字符的上边界用十六进制表示就是FFFF ,因此任何大于该数字的码位必须用两个码元(共32位)表示

【String.fromCodePoint()】

  ES5提供的String.fromCharCode方法,用于从码位返回对应字符,但是这个方法不能识别32位的UTF-16字符

  ECMAScript通常会提供正反两种方法。可以使用codePointAt() 来提取字符串内中某个字符的码位,也可以借助String.fromCodePoint()根据给定的码位来生成一个字符

console.log(String.fromCharCode(0x20bb7)); // "?"
console.log(String.fromCodePoint(0x20bb7)); // "??"
console.log(String.fromCharCode(0x0bb7)); // "?"

  上面代码中,String.fromCharCode不能识别大于0xFFFF的码位,所以0x20BB7就发生了溢出,最高位2被舍弃了,最后返回码位U+0BB7对应的字符,而不是码位U+20BB7对应的字符

  如果String.fromCodePoint()方法有多个参数,则它们会被合并成一个字符串返回

// true
String.fromCodePoint(0x78, 0x1f680, 0x79) === ‘x\uD83D\uDE80y‘

  可以将 String.fromCodePoint() 视为 String.fromCharCode() 的完善版本。两者处理 BMP 字符时会返回相同结果,只有处理 BMP 范围之外的字符时才会有差异

for...of

  对于32位的辅助平面字符来说,使用for或for in循环,可能得不到正确的结果

var s = ‘??a‘;
for (let ch in s) {
  console.log(s[ch]);
}
//?
//?
//a

  而for...of循环可以正确的识别32位的UTF-16字符

var s = ‘??a‘;
for (let ch of s) {
  console.log(ch);
}
//??
//a

normalize()

  许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode提供了两种方法。一种是直接提供带重音符号的字符,比如ǒ(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(\u004F)和ˇ(\u030C)合成ǒ(\u004F\u030C)

  这两种表示方法,在视觉和语义上都等价,但是JavaScript不能识别

console.log(‘\u01D1‘===‘\u004F\u030C‘); //false

console.log(‘\u01D1‘.length); // 1
console.log(‘\u004F\u030C‘.length); // 2

  上面代码表示,JavaScript将合成字符视为两个字符,导致两种表示方法不相等。

  ES6提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为Unicode正规化

console.log(‘\u01D1‘===‘\u01D1‘.normalize()); //true
console.log(‘\u01D1‘=== ‘\u004F\u030C‘.normalize()); //true

  normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下

  1、NFC,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价

console.log(‘\u01D1‘===‘\u01D1‘.normalize("NFC")); //true
console.log(‘\u01D1‘=== ‘\u004F\u030C‘.normalize("NFC")); //true

  2、NFD,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符

console.log(‘\u004F\u030C‘===‘\u01D1‘.normalize("NFD")); //true
console.log(‘\u004F\u030C‘=== ‘\u004F\u030C‘.normalize("NFD")); //true

  3、NFKC,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,normalize方法不能识别中文。)

  4、NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符

  在开发国际化应用时,normalize() 方法非常有用。但normalize()方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过Unicode编号区间判断

U修饰符

  正则表达式可以完成简单的字符串操作,但默认将字符串中的每一个字符按照16位编码处理。为了解决这个问题, ES6 对正则表达式添加了u修饰符,含义为“Unicode模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码

/^\uD83D/u.test(‘\uD83D\uDC2A‘) // false
/^\uD83D/.test(‘\uD83D\uDC2A‘) // true

  一旦为正则表达式设置了 u 修饰符,正则表达式将会识别32位的辅助平面字符为1个字符,而不是两个

【点号】

  点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码位大于0xFFFF的 Unicode 字符,点字符不能识别,必须加上u修饰符

var text = "??" ;
console.log(text.length); // 2
console.log(/^.$/.test(text));//false
console.log(/^.$/u.test(text)); //true

【大括号】

  ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词

/\u{61}/.test(‘a‘) // false
/\u{61}/u.test(‘a‘) // true
/\u{20BB7}/u.test(‘??‘) // true

【量词】

  使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符

/a{2}/.test(‘aa‘) // true
/a{2}/u.test(‘aa‘) // true
/??{2}/.test(‘????‘) // false
/??{2}/u.test(‘????‘) // true

【预定义模式】

  u修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF的 Unicode 字符

/^\S$/.test(‘??‘) // false
/^\S$/u.test(‘??‘) // true

【字符串长度】

  上面代码的\S是预定义模式,匹配所有不是空格的字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的 Unicode 字符

  虽然ES6不支持字符串码位数量的检测,length属性仍然返回字符串编码单元的数量。利用[\s\S],再加上u修饰符,就可以写出一个正确返回字符串长度的函数

function codePointLength(text) {
  var result = text.match(/[\s\S]/gu);
  return result ? result.length : 0;
}

var s = ‘????‘;

console.log(s.length); // 4
console.log(codePointLength(s)); // 2

【检测支持】

  u修饰符是语法层面的变更,尝试在不兼容 ES6 的 JS 引擎中使用它会抛出语法错误。如果要检测当前引擎是否支持u修饰符,最安全的方式是通过以下函数来判断

function hasRegExpU() {
    try {
        var pattern = new RegExp(".", "u");
        return true;
    } catch (ex) {
        return false;
    }
}

  这个函数使用了RegExp构造函数并传入字符串‘u‘作为参数,该语法即使在旧版 JS 引擎中也是有效的。但是,如果当前引擎不支持u修饰符则会抛出错误

时间: 2024-10-07 00:15:28

ES6关于Unicode的相关扩展的相关文章

ES6字符串相关扩展

变量的解构赋值 // 数组的解构赋值 let [a,b,c] = [1,2,3]; //1,2,3 let [a,b,c] = [,123,]; //undefined 123 undefined let [a=111,b,c] = [,123,]; //111 123 undefined console.log(a,b,c); 对象的解构赋值 let {foo,bar} = {foo : 'hello',bar : 'hi'};//hello hi let {foo,bar} = {bar :

ES6随笔--各数据类型的扩展(1) --字符串和正则

ES6随笔--基本数据类型的扩展 字符串 Unicode表示法 \uxxxx表示的Unicode如果码数超过了0xFFFF的范围,只要放进大括号内就能识别: codePointAt() 用codePointAt()可以准确识别码数超出0xFFFF范围的Unicode字符: for...of循环能够正确识别32位的UTF-16字符: String.fromCodePoint() 与codePointAt()作用刚好相反:根据传入的Unicede码数返回对应的字符: 遍历 for...of循环可以循

yum 安装php7和相关扩展

安装php7和相关扩展 rpm -Uvh http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm rpm -ivh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm   (nginx) rpm -Uvh http://rpms.famillecollet.com/

ES6随笔--各数据类型的扩展(2)--数值

ES6随笔--各数据类型的扩展(2)--数值 二进制和八进制表示 二进制0b(或0B)表示:八进制0o(或0O)表示: 这两种字符串转换成十进制数值,使用Number()方法: Number('0b111') // 7 Number.isFinite(), Number.isNaN(); Number.isFinite()的参数如果不是数值,一律返回false, Infinity/-Infinity/NaN都返回false: Number.isNaN()对不是NaN的参数一律返回false; 这

ES6随笔--各数据类型的扩展(3)--函数

ES6随笔--各数据类型的扩展(3)--函数 1. 函数参数的默认值 可以在函数参数中指定默认值,函数内不能再用let或const声明: 使用参数默认值时,不能出现同名参数: 参数默认值惰性求值,每次调用函数会重新计算参数默认值表达式: let x = 99; function foo(p = x + 1) { console.log(p); } foo() // 100 x = 100; foo() // 101 可以与解构赋值默认值结合使用: function ({x, y = 5} = {

JavaScript学习笔记--ES6学习(五) 数值的扩展

ES6 对于数值类型 (Number) 进行了一下扩展: 1.对于二进制和八进制提供了新的写法 ES6对于二进制和八进制的数值提供了新的写法,分别用0b (或者0B) 和0o (或者0o) 表示.例如: 0b111110111 === 503 // true 0o767 === 503 //true 由于从ES5开始,严格模式中,八进制不再允许使用前缀0来表示,因此在ES6中进一步明确,要用0o来表示. 如果要将0b和0o前缀的字符串数值转换为十进制,要使用Number方法, 例如: var i

ES6学习笔记(二)——字符串扩展

相信很多人也和我一样,不喜欢这样循规蹈矩的逐条去学习语法,很枯燥乏味.主要是这样学完一遍之后,没过一段时间就忘到九霄云外了.不如实际用到的时候研究它记得牢靠,所以我就整理成笔记,加深记忆的同时便于复习查看. 在这样不断的学习过程中,也提高了自己的总结能力:) 1.字符串的遍历器接口 ES5,数组和对象可以进行遍历,使用for() 和 for...in,还有jq中的each()方法进行遍历. ES6为我们提供了字符串遍历器  for...of 循环遍历 优点:可以识别大于0xFFFF的码点,传统的

ES6 入门系列 - 函数的扩展

1函数参数的默认值 基本用法 在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法. function log(x, y) { y = y || 'World'; console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello World 上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World.这种写法的

ES6中对数组的扩展

Array.from()方法用于将两类对象转为真正的数组:类似数组的对象和可遍历对象. php有种返回json长成:let arr1={0:'1',1:'a',2:'c'};ES6可以直接写成let arr2=Array.from(arr1);//['1','a','c'].ES5可以写成 var arr3=[].slice.call(arr1);感谢ES6吧. 只要是部署了Iterator接口的数据结构,Array.from都能将其转为数组.Array.from('hello')//[h,e,