【读书笔记】【深入理解ES6】#2-字符串和正则表达式

更好的Unicode支持

在ES6出现以前,JS字符串一直基于16位字符编码(UTF-16)进行构建。
每16位的序列是一个编码单元(code unit),代表一个字符。
lengthcharAt() 等字符串属性和方法都是基于这种编码单元构造的。
Unicode引入扩展字符集后,16位的序列不足以表示所有的字符,编码规则不得不进行改变。

UTF-16码位(code point)

在UTF-16中,前 2^16 个码位均以16位的编码单元表示,这个范围被称作基本多文种平面(BMP,Basic Multilingual Plane)
超出这个范围的码位则要归属于某个辅助平面(supplementary plane),其中的码位仅用16位就无法表示了。
为此,UTF-16引入了代理对(surrogate pair),其规定用两个16位编码单元表示一个码位。

这也就是说字符串里的字符有两种
1. 由一个编码单元16位表示的BMP字符
2. 由两个编码单元32位表示的辅助平面字符

在ES5中,所有字符串的操作都是基于16位编码单元。如果采用同样的方式处理包含代理对的UTF-16编码字符,得到的结果可能与预期不符。

var ji = String.fromCodePoint(134071);

console.log(ji); // 
// 期望的长度是1,但是由于使用代理对表示的,返回值为2
console.log(ji.length); // 2
// 匹配一个字符的正则表达式失败
console.log(/^.$/.test(ji)); // false
// 前后两个16位的编码单元都不表示任何可打印的字符,所以都不返回合法的字符
console.log(ji.charAt(0)); // �
console.log(ji.charAt(1)); // �
// charCodeAt返回每个16位编码单元对应的数值
console.log(ji.charCodeAt(0)); // 55362
console.log(ji.charCodeAt(1)); // 57271

在ES6中强制使用UTF-16字符串编码来解决上述问题。

codePointAt() 方法

该方法为ES6新增方法,接受编码单元的位置而非字符位置作为参数,返回与字符串中给定位置对应的码位,即一个整数值。

var jia = String.fromCodePoint(134071) + "a";

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

// 返回完整的码位,即使这个码位包含多个编码单元
console.log(jia.codePointAt(0)); // 134071
// 返回值同charCodeAt相同
console.log(jia.codePointAt(1)); // 57271
// BMP字符集字符,返回值同charCodeAt相同
console.log(jia.codePointAt(2)); // 97

对于BMP字符集中的字符,codePointAt() 方法的返回值与 charCodeAt() 方法的相同,而对于非BMP字符集来说返回值则不同。

使用 codePointAt 方法检测一个字符占用的编码单元数量。

var ji = String.fromCodePoint(134071);

function is32Bit(c) {
    return c.codePointAt(0) > 0xFFFF;
}

console.log(is32Bit(ji)); // true
console.log(is32Bit("a")); // false

String.fromCodePoint() 方法

根据指定的码位生成一个字符。

console.log(String.fromCodePoint(134071)); // ""

normalize() 方法

提供Unicode的标准化形式。

在对比字符串之前,一定先把它们标准化为同一种形式。

看一个示例,比较两个字符 “切” 和 “” 的大小 。

var ji = String.fromCodePoint(134071);

console.log("切".codePointAt(0)); // 64000
console.log(ji.codePointAt(0)); // 134071

console.log("切".charCodeAt(0)); // 64000
console.log(ji.charCodeAt(0)); // 55362

console.log("切" < ji); // false
console.log("切".normalize() < ji.normalize()); // true

“切” 的 Unicode 是 64000,”” 的 Unicode 是 134071
字符串比较时,”切” 应该小于 “” 才对,但是比较结果是false
而对字符串使用 normalize() 方法标准化之后再比较就和预想的结果一致了。

正则表达式 u 修饰符

正则表达式可以完成简单的字符串操作,但默认将字符串中的每一个字符按照16位编码单元处理。
为解决这个问题,ES6给正则表达式定义了一个支持Unicode的 u 修饰符。

var ji = String.fromCodePoint(134071);

console.log(ji.length); // 2
console.log(/^.$/.test(ji)); // false
console.log(/^.$/u.test(ji)); // true

正则表达式 /^.$/ 匹配所有单字节字符串。
没有使用 u 修饰符时,会匹配编码单元;
使用 u 修饰符时,会匹配字符。

计算码位数量

ES6不支持字符串码位数量的检测(length属性仍然返回字符串编码单元的数量)。
但使用 u 修饰符后,可以通过正则表达式计算出码位的数量。

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

var ji = String.fromCodePoint(134071);
console.log("abc".length); // 3
console.log((ji + "bc").length); // 4

console.log(codePointLength("abc")); // 3
console.log(codePointLength(ji + "bc")); // 3

NOTE

这个方法虽然有效,但是统计长字符串中的码位数量时,运行效率很低。

检测 u 修饰符支持

u 修饰符是语法层面的变更,尝试在不兼容ES6的JS引擎中使用它会导致语法错误。

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

其它字符串变更

字符串中的子串识别

ES6提供了以下3个新方法来识别字符串中的子串。

  • includes() 方法

    如果在字符串中检测到指定文本则返回true,否则返回false

  • startsWith() 方法

    如果在字符串的起始部分检测到指定文本则返回true,否则返回false

  • endsWith() 方法

    如果在字符串的结束部分检测到指定文本则返回true,否则返回false

以上方法都接受两个参数

  1. 指定要搜索的文本
  2. 可选参数 指定一个开始搜索的位置的索引值
var msg = "Hello World!";

console.log(msg.startsWith("Hello")); // true
console.log(msg.endsWith("!")); // true
console.log(msg.includes("o")); // true

console.log(msg.startsWith("o")); // false
console.log(msg.endsWith("world!")); // false
console.log(msg.includes("x")); // false

console.log(msg.startsWith("o", 4)); // true
console.log(msg.endsWith("o", 8)); // true
console.log(msg.includes("o", 8)); // false

repeat() 方法

接受一个 number 类型的参数,表示该字符串重复的次数;
返回值是当前字符串重复一定次数后的新字符串。

console.log("x".repeat(3)); // "xxx"
console.log("hello".repeat(2)); // "hellohello"
console.log("abc".repeat(4)); // "abcabcabcabc"

其它正则表达式语法变更

正则表达式 y 修饰符

它会影响正则表达式搜索过程中的 sticky 属性,当在字符串中开始字符匹配时,它会通知搜索从正则表达式的 lastIndex 属性开始进行,如果在指定位置没有成功匹配,则停止继续匹配。

var text = "hello1 hello2 hello3",
    pattern = /hello\d\s?/,
    result = pattern.exec(text),
    globalPattern = /hello\d\s?/g,
    globalResult = globalPattern.exec(text),
    stickyPattern = /hello\d\s?/y,
    stickyResult = stickyPattern.exec(text);

console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello1 "
console.log(stickyResult[0]); // "hello1 "

pattern.lastIndex = 1;
globalPattern.lastIndex = 1;
stickyPattern.lastIndex = 1;

result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);

// 没有修饰符的正则表达式忽略 lastIndex 的变化
console.log(result[0]); // "hello1 "
// 使用g修饰符的正则表达式,从e开始搜索,匹配"hello2 "
console.log(globalResult[0]); // "hello2 "
// 使用y修饰符的粘滞正则表达式,从e开始匹配不到相应字符串,就此终止,stickyResult的值为null
console.log(stickyResult[0]); // 抛出错误:Uncaught TypeError: Cannot read property '0' of null

当执行操作时,y 修饰符会把上次匹配后面一个字符的索引保存在 lastIndex 中;
如果该操作匹配的结果为空,则 lastIndex 会被重置为0.
g 修饰符的行为与此相同。

var text = "hello1 hello2 hello3",
    pattern = /hello\d\s?/,
    result = pattern.exec(text),
    globalPattern = /hello\d\s?/g,
    globalResult = globalPattern.exec(text),
    stickyPattern = /hello\d\s?/y,
    stickyResult = stickyPattern.exec(text);

console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello1 "
console.log(stickyResult[0]); // "hello1 "

console.log(pattern.lastIndex); // 0
console.log(globalPattern.lastIndex); // 7
console.log(stickyPattern.lastIndex); // 7

result = pattern.exec(text);
globalResult = globalPattern.exec(text);
stickyResult = stickyPattern.exec(text);

console.log(result[0]); // "hello1 "
console.log(globalResult[0]); // "hello2 "
console.log(stickyResult[0]); // "hello2 "

console.log(pattern.lastIndex); // 0
console.log(globalPattern.lastIndex); // 14
console.log(stickyPattern.lastIndex); // 14

关于 y 修饰符还需要记住两点:
1. 只有调用 exec()test() 这些正则表达式对象的方法时才会涉及 lastIndex 属性;调用字符串的方法,例如 match(),则不会触发粘滞行为。
2. 对于粘滞正则表达式而言,如果使用 ^ 字符来匹配字符串开端,只会从字符串的起始位置或者多行模式的首行进行匹配。
lastIndex 为0时,是否使用粘滞正则表达式并无差别;
如果 lastIndex 不为0,则该表达式永远不会匹配到正确结果。

检测 y 修饰符是否存在

var pattern = /hello\d/y;
console.log(pattern.sticky); // true

如果JS引擎支持粘滞正则表达式,则 sticky 属性为true,否则为false。这个属性是只读的。

检测JS引擎是否支持y修饰符

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

正则表达式的复制

var re1 = /ab/i,
    re2 = new RegExp(re1);

ES6增加了一个可选参数,为正则表达式指定一个修饰符。

var re1 = /ab/i, // 使用i修饰符,添加大小写无关的特性
    // ES5中会抛出错误,ES6中正常运行
    re2 = new RegExp(re1, "g"); // 使用g修饰符代替i

console.log(re1.toString()); // "/ab/i"
console.log(re2.toString()); // "/ab/g"

console.log(re1.test("ab")); // true
console.log(re2.test("ab")); // true

console.log(re1.test("AB")); // true
console.log(re2.test("AB")); // false

flags属性

获取正则表达式所使用的修饰符。

var re = /ab/g;
console.log(re.source); // "ab"
console.log(re.flags); // "g"

模板字面量

基础语法

var message = `Hello world!`;
console.log(message); // Hello world!
console.log(typeof message); // string
console.log(message.length); // 12

反撇号的转义(使用反斜杠)

var message = `\`Hello\` world!`;
console.log(message); // `Hello` world!
console.log(typeof message); // string
console.log(message.length); // 14

单、双引号不需要转义。

多行字符串

var message = `Multiline
string`;

console.log(message); // Multiline
                      // string
console.log(message.length); // 16

反撇号中的所有空白字符都属于字符串的一部分,所以千万要注意缩进。

也可以现实的使用 \n 来实现换行。

var message = `Multiline\nstring`;

字符串占位符

模板字面量中可以使用占位符功能。

var name = "Nicholas",
    message = `Hello, ${name}.`;
console.log(message); // "Hello, Nicholas."

模板字面量可以访问作用域中所有可访问的变量,无论是在严格模式还是非严格模式,尝试嵌入一个未定义的变量总是会抛出错误。

除变量外,还可以嵌入其它内容,如运算式、函数调用等。

var count = 10,
    price = 0.25,
    message = `${count} items cost $${(count * price).toFixed(2)}.`;
console.log(message); // "10 items cost $2.50."

可以在一个模板字面量中嵌入另一个模板字面量。

var name = "Nicholas",
    message = `Hello, ${
        `my name is ${name}`
    }.`;
console.log(message); // "Hello, my name is Nicholas."

标签模板

每个模板标签都可以执行模板字面量上的转换并返回最终的字符串值。
标签指的是在模板字面量第一个反撇号(`)前方标注的字符串。

var message = tag`Hello world`;

定义标签

标签可以是一个函数。

  • 第一个参数是一个数组,包含JS解释过后的字面量字符串
  • 之后的所有参数都是每一个占位符的解释值
function tag(literals, ...substitutions) {

}

使用下面具体的例子来说明这两个参数的值。

function passthru(literals, ...substitutions) {
    var result = "";

    for (var i = 0; i < substitutions.length; i++) {
        result += literals[i];
        result += substitutions[i];
    }

    result += literals[literals.length -1];

    return result;
}

var count = 10,
    price = 0.25,
    message = passthru`${count} items cost $${(count * price).toFixed(2)}.`;

console.log(message); // 10 items cost $2.50.

literals 的值:

[
  "", // 第一个占位符前的空字符串
  " items cost $", // 第一、二个占位符之间的字符串
  "." // 第二个占位符后的字符串
]

substitutions 的值:

[
  10, // 第一个占位符的值
  "2.50" // 第二个占位符的值
]

literals 的数量总是比 substitutions 多一个。
上面例子中的 passthru 方法模拟了模板字面量的默认行为。

在模板字面量中使用原始值

使用内建的 String.raw() 标签:

var message1 = `Multiline\nstring`,
    message2 = String.raw`Multiline\nstring`;

console.log(message1); // "Multiline
                       // string"
console.log(message2); // "Multilinenstring"

message1 中的 \n 被解释为一个新行,message2 中的 \n 则以原始值现实。

本文是全系列中第2 / 13篇:《深入理解ES6》

© 2017 – 2019, 佳佳. 版权所有. 未经作者同意,严禁转载。

发表评论

电子邮件地址不会被公开。 必填项已用*标注