正则表达式的其它改动

正则表达式是在 JS 中操作字符串的重要方面之一,与该语言的其他方面相似,它在以往的版本中并未有太多改变。不过,为了配合字符串的更新,ES6 也对正则表达式进行了一些改进。

正则表达式 y 标志

在 Firefox 实现了对正则表达式 y 标志的专有扩展之后,ES6 将该实现标准化。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);

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

此例中有三个正则表达式:pattern 中的表达式没有使用任何标志,globalPattern 使用了 g 标志,stickyPattern 则使用了 y 标志。对 console.log() 的第一次调用,三个正则表达式分别都返回了 "hello1 ",此字符串尾部有个空格。

此后,三个模式的 lastIndex 属性全部被更改为 1,表示三个模式的正则表达式都应当从第二个字符开始尝试匹配。不使用任何标志的正则表达式完全忽略了对于 lastIndex 的更改,仍然毫无意外地匹配了 "hello1 ";而使用 g 标志的正则表达式继续匹配了 "hello2 ",因为它从第二个字符("e")开始,持续向着字符串尾部方向搜索;粘连的正则表达式则在第二个字符处没有匹配成功,因此 stickyResult 的值是 null

一旦匹配操作成功,粘连标志就会将匹配结果之后的那个字符的索引值保存在 lastIndex 中;若匹配未成功,那么 lastIndex 的值将重置为 0。全局标志的行为与其相同,如下所示:

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

对于 stickyPatternglobalPattern 模式变量来说,第一次调用之后 lastIndex 的值均被更改为 7,而第二次则均被改为 14。

有两个关于粘连标志的微妙细节需要牢记:

  1. 只有调用正则表达式对象上的方法(例如 exec()test() 方法),lastIndex 属性才会生效。而将正则表达式作为参数传递给字符串上的方法(例如 match()),并不会体现粘连特性。

  2. 当使用 ^ 字符来匹配字符串的起始处时,粘连的正则表达式只会匹配字符串的起始处(或者在多行模式下匹配行首)。当 lastIndex 为 0 时,^ 不会让粘连的正则表达式 与非粘连的有任何区别;而当 lastIndex 在单行模式下不对应整个字符串起始处,或者当它在多行模式下不对应行首时,粘连的正则表达式永远不会匹配成功。

和正则表达式其他标志相同,你可以根据一个属性来检测 y 标志是否存在。此时你需要检查的是 sticky 属性,如下:

var pattern = /hello\d/y;

console.log(pattern.sticky);    // true

如果粘连标志存在,那么 sticky 属性的值会被设为 true,否则会被设为 falsesticky 属性由 y 标志存在与否决定,是只读的,它的值不能在代码中修改。

u 标志相似,y 标志也是个语法变更,所以在旧版 JS 引擎中它会造成语法错误。你可以用如下方法来检测它是否被支持:

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

此函数类似于对 u 标志的检查,在无法使用 y 标志来创建正则表达式时会返回 false 。同样,如果需要在旧版 JS 引擎中运行的代码中使用 y 标志,请确保使用 RegExp 构造器来定义正则表达式,以避免语法错误。

复制正则表达式

在 ES5 中,你可以将正则表达式传递给 RegExp 构造器来复制它,就像这样:

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

re2 变量只是 re1 的一个副本。但如果你向 RegExp 构造器传递了第二个参数,即正则表达式的标志,那么该代码就无法工作,正如该范例:

var re1 = /ab/i,

// throws an error in ES5, okay in ES6
re2 = new RegExp(re1, "g");

如果你在 ES5 环境中运行这段代码,那么你会收到一条错误信息,表示在第一个参数已经是正则表达式的情况下不能再使用第二个参数。ES6 则修改了这个行为,允许使用第二个参数,并且让它覆盖第一个参数中的标志。例如:

var re1 = /ab/i,

// throws an error in ES5, okay in ES6
re2 = new RegExp(re1, "g");

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

此代码中的 re1 带有忽略大小写的 i 标志,而 re2 则只带有全局的 g 标志。RegExp 构造器复制了 re1 的模式并用 g 标志替换了 i 标志。如果没有第二个参数,re2 就会拥有与 re1 相同的标志。

flags 属性

在新增了一个标志并且对如何使用标志进行更改之余,ES6 还新增了一个与之关联的属性。在 ES5 中,你可以使用 source 属性来获取正则表达式的文本,但若想获取标志字符串,你必须解析 toString() 方法的输出,就像下面展示的那样:

function getFlags(re) {
    var text = re.toString();
    return text.substring(text.lastIndexOf("/") + 1, text.length);
}

// toString() is "/ab/g"
var re = /ab/g;

console.log(getFlags(re));          // "g"

此处将正则表达式转换为一个字符串,并返回了最后一个 / 之后的字符,这些字符即为标志。

ES6 新增了 flags 属性用于配合 source 属性,让标志的获取变得更容易。这两个属性均为只有 getter 的原型访问器属性,因此都是只读的。flags 属性使得检查正则表达式更容易,有助于调试与继承。

后期加入 ES6 的 flags 属性,会返回正则表达式中所有标志组成的字符串形式。例如:

var re = /ab/g;

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

本例查找了 re 的所有标志并将其打印到控制台,所用的代码量要比 toString() 方式少得多。同时使用 sourceflags 允许你直接提取正则表达式的组成部分,而不必将正则表达式转换为字符串。

本章介绍过的字符串与正则表达式的改进已绝对强大,然而 ES6 在字符串方面还有更大的进步,它引入了一种新的字面量形式让字符串更加灵活。