JavaScript高级程序设计(第4版)
上QQ阅读APP看书,第一时间看更新

5.2 RegExp

ECMAScript通过RegExp类型支持正则表达式。正则表达式使用类似Perl的简洁语法来创建:

    let expression = /pattern/flags;

这个正则表达式的pattern(模式)可以是任何简单或复杂的正则表达式,包括字符类、限定符、分组、向前查找和反向引用。每个正则表达式可以带零个或多个flags(标记),用于控制正则表达式的行为。下面给出了表示匹配模式的标记。

❑ g:全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束。

❑ i:不区分大小写,表示在查找匹配时忽略pattern和字符串的大小写。

❑ m:多行模式,表示查找到一行文本末尾时会继续查找。

❑ y:粘附模式,表示只查找从lastIndex开始及之后的字符串。

❑ u: Unicode模式,启用Unicode匹配。

❑ s:dotAll模式,表示元字符.匹配任何字符(包括\n或\r)。

使用不同模式和标记可以创建出各种正则表达式,比如:

    // 匹配字符串中的所有"at"
    let pattern1 = /at/g;
    // 匹配第一个"bat"或"cat",忽略大小写
    let pattern2 = /[bc]at/i;
    // 匹配所有以"at"结尾的三字符组合,忽略大小写
    let pattern3 = /.at/gi;

与其他语言中的正则表达式类似,所有元字符在模式中也必须转义,包括:

    ( [ { \ ^ $ | ) ] } ? * + .

元字符在正则表达式中都有一种或多种特殊功能,所以要匹配上面这些字符本身,就必须使用反斜杠来转义。下面是几个例子:

    // 匹配第一个"bat"或"cat",忽略大小写
    let pattern1 = /[bc]at/i;
    // 匹配第一个"[bc]at",忽略大小写
    let pattern2 = /\[bc\]at/i;
    // 匹配所有以"at"结尾的三字符组合,忽略大小写
    let pattern3 = /.at/gi;
    // 匹配所有".at",忽略大小写
    let pattern4 = /\.at/gi;

这里的pattern1匹配"bat"或"cat",不区分大小写。要直接匹配"[bc]at",左右中括号都必须像pattern2中那样使用反斜杠转义。在pattern3中,点号表示"at"前面的任意字符都可以匹配。如果想匹配".at",那么要像pattern4中那样对点号进行转义。

前面例子中的正则表达式都是使用字面量形式定义的。正则表达式也可以使用RegExp构造函数来创建,它接收两个参数:模式字符串和(可选的)标记字符串。任何使用字面量定义的正则表达式也可以通过构造函数来创建,比如:

    // 匹配第一个"bat"或"cat",忽略大小写
    let pattern1 = /[bc]at/i;
    // 跟pattern1 一样,只不过是用构造函数创建的
    let pattern2 = new RegExp("[bc]at", "i");

这里的pattern1和pattern2是等效的正则表达式。注意,RegExp构造函数的两个参数都是字符串。因为RegExp的模式参数是字符串,所以在某些情况下需要二次转义。所有元字符都必须二次转义,包括转义字符序列,如\n(\转义后的字符串是\\,在正则表达式字符串中则要写成\\\\)。下表展示了几个正则表达式的字面量形式,以及使用RegExp构造函数创建时对应的模式字符串。

此外,使用RegExp也可以基于已有的正则表达式实例,并可选择性地修改它们的标记:

    const re1 = /cat/g;
    console.log(re1);   // "/cat/g"
    const re2 = new RegExp(re1);
    console.log(re2);   // "/cat/g"
    const re3 = new RegExp(re1, "i");
    console.log(re3);   // "/cat/i"

5.2.1 RegExp实例属性

每个RegExp实例都有下列属性,提供有关模式的各方面信息。

❑ global:布尔值,表示是否设置了g标记。

❑ ignoreCase:布尔值,表示是否设置了i标记。

❑ unicode:布尔值,表示是否设置了u标记。

❑ sticky:布尔值,表示是否设置了y标记。

❑ lastIndex:整数,表示在源字符串中下一次搜索的开始位置,始终从0开始。

❑ multiline:布尔值,表示是否设置了m标记。

❑ dotAll:布尔值,表示是否设置了s标记。

❑ source:正则表达式的字面量字符串(不是传给构造函数的模式字符串),没有开头和结尾的斜杠。

❑ flags:正则表达式的标记字符串。始终以字面量而非传入构造函数的字符串模式形式返回(没有前后斜杠)。

通过这些属性可以全面了解正则表达式的信息,不过实际开发中用得并不多,因为模式声明中包含这些信息。下面是一个例子:

    let pattern1 = /\[bc\]at/i;
    console.log(pattern1.global);        // false
    console.log(pattern1.ignoreCase);   // true
    console.log(pattern1.multiline);    // false
    console.log(pattern1.lastIndex);    // 0
    console.log(pattern1.source);        // "\[bc\]at"
    console.log(pattern1.flags);         // "i"
    let pattern2 = new RegExp("\\[bc\\]at", "i");
    console.log(pattern2.global);        // false
    console.log(pattern2.ignoreCase);   // true
    console.log(pattern2.multiline);    // false
    console.log(pattern2.lastIndex);    // 0
    console.log(pattern2.source);        // "\[bc\]at"
    console.log(pattern2.flags);         // "i"

注意,虽然第一个模式是通过字面量创建的,第二个模式是通过RegExp构造函数创建的,但两个模式的source和flags属性是相同的。source和flags属性返回的是规范化之后可以在字面量中使用的形式。

5.2.2 RegExp实例方法

RegExp实例的主要方法是exec(),主要用于配合捕获组使用。这个方法只接收一个参数,即要应用模式的字符串。如果找到了匹配项,则返回包含第一个匹配信息的数组;如果没找到匹配项,则返回null。返回的数组虽然是Array的实例,但包含两个额外的属性:index和input。index是字符串中匹配模式的起始位置,input是要查找的字符串。这个数组的第一个元素是匹配整个模式的字符串,其他元素是与表达式中的捕获组匹配的字符串。如果模式中没有捕获组,则数组只包含一个元素。来看下面的例子:

    let text = "mom and dad and baby";
    let pattern = /mom( and dad( and baby)? )? /gi;
    let matches = pattern.exec(text);
    console.log(matches.index);    // 0
    console.log(matches.input);    // "mom and dad and baby"
    console.log(matches[0]);        // "mom and dad and baby"
    console.log(matches[1]);        // " and dad and baby"
    console.log(matches[2]);        // " and baby"

在这个例子中,模式包含两个捕获组:最内部的匹配项" and baby",以及外部的匹配项" and dad"或" and dad and baby"。调用exec()后找到了一个匹配项。因为整个字符串匹配模式,所以matchs数组的index属性就是0。数组的第一个元素是匹配的整个字符串,第二个元素是匹配第一个捕获组的字符串,第三个元素是匹配第二个捕获组的字符串。

如果模式设置了全局标记,则每次调用exec()方法会返回一个匹配的信息。如果没有设置全局标记,则无论对同一个字符串调用多少次exec(),也只会返回第一个匹配的信息。

    let text = "cat, bat, sat, fat";
    let pattern = /.at/;
    let matches = pattern.exec(text);
    console.log(matches.index);        // 0
    console.log(matches[0]);           // cat
    console.log(pattern.lastIndex);   // 0
    matches = pattern.exec(text);
    console.log(matches.index);        // 0
    console.log(matches[0]);           // cat
    console.log(pattern.lastIndex);   // 0

上面例子中的模式没有设置全局标记,因此调用exec()只返回第一个匹配项("cat")。lastIndex在非全局模式下始终不变。

如果在这个模式上设置了g标记,则每次调用exec()都会在字符串中向前搜索下一个匹配项,如下面的例子所示:

    let text = "cat, bat, sat, fat";
    let pattern = /.at/g;
    let matches = pattern.exec(text);
    console.log(matches.index);        // 0
    console.log(matches[0]);           // cat
    console.log(pattern.lastIndex);   // 3
    matches = pattern.exec(text);
    console.log(matches.index);        // 5
    console.log(matches[0]);           // bat
    console.log(pattern.lastIndex);   // 8
    matches = pattern.exec(text);
    console.log(matches.index);        // 10
    console.log(matches[0]);           // sat
    console.log(pattern.lastIndex);   // 13

这次模式设置了全局标记,因此每次调用exec()都会返回字符串中的下一个匹配项,直到搜索到字符串末尾。注意模式的lastIndex属性每次都会变化。在全局匹配模式下,每次调用exec()都会更新lastIndex值,以反映上次匹配的最后一个字符的索引。

如果模式设置了粘附标记y,则每次调用exec()就只会在lastIndex的位置上寻找匹配项。粘附标记覆盖全局标记。

    let text = "cat, bat, sat, fat";
    let pattern = /.at/y;
    let matches = pattern.exec(text);
    console.log(matches.index);         // 0
    console.log(matches[0]);             // cat
    console.log(pattern.lastIndex);    // 3
    // 以索引3 对应的字符开头找不到匹配项,因此exec()返回null
    // exec()没找到匹配项,于是将lastIndex设置为0
    matches = pattern.exec(text);
    console.log(matches);                // null
    console.log(pattern.lastIndex);    // 0
    // 向前设置lastIndex可以让粘附的模式通过exec()找到下一个匹配项:
    pattern.lastIndex = 5;
    matches = pattern.exec(text);
    console.log(matches.index);         // 5
    console.log(matches[0]);             // bat
    console.log(pattern.lastIndex);    // 8

正则表达式的另一个方法是test(),接收一个字符串参数。如果输入的文本与模式匹配,则参数返回true,否则返回false。这个方法适用于只想测试模式是否匹配,而不需要实际匹配内容的情况。test()经常用在if语句中:

    let text = "000-00-0000";
    let pattern = /\d{3}-\d{2}-\d{4}/;
    if (pattern.test(text)) {
      console.log("The pattern was matched.");
    }

在这个例子中,正则表达式用于测试特定的数值序列。如果输入的文本与模式匹配,则显示匹配成功的消息。这个用法常用于验证用户输入,此时我们只在乎输入是否有效,不关心为什么无效。

无论正则表达式是怎么创建的,继承的方法toLocaleString()和toString()都返回正则表达式的字面量表示。比如:

    let pattern = new RegExp("\\[bc\\]at", "gi");
    console.log(pattern.toString());         // /\[bc\]at/gi
    console.log(pattern.toLocaleString()); // /\[bc\]at/gi

这里的模式是通过RegExp构造函数创建的,但toLocaleString()和toString()返回的都是其字面量的形式。

注意 正则表达式的valueOf()方法返回正则表达式本身。

5.2.3 RegExp构造函数属性

RegExp构造函数本身也有几个属性。(在其他语言中,这种属性被称为静态属性。)这些属性适用于作用域中的所有正则表达式,而且会根据最后执行的正则表达式操作而变化。这些属性还有一个特点,就是可以通过两种不同的方式访问它们。换句话说,每个属性都有一个全名和一个简写。下表列出了RegExp构造函数的属性。

通过这些属性可以提取出与exec()和test()执行的操作相关的信息。来看下面的例子:

    let text = "this has been a short summer";
    let pattern = /(.)hort/g;
    if (pattern.test(text)) {
      console.log(RegExp.input);          // this has been a short summer
      console.log(RegExp.leftContext);   // this has been a
      console.log(RegExp.rightContext); // summer
      console.log(RegExp.lastMatch);     // short
      console.log(RegExp.lastParen);     // s
    }

以上代码创建了一个模式,用于搜索任何后跟"hort"的字符,并把第一个字符放在了捕获组中。不同属性包含的内容如下。

❑ input属性中包含原始的字符串。

❑ leftConext属性包含原始字符串中"short"之前的内容,rightContext属性包含"short"之后的内容。

❑ lastMatch属性包含匹配整个正则表达式的上一个字符串,即"short"。

❑ lastParen属性包含捕获组的上一次匹配,即"s"。

这些属性名也可以替换成简写形式,只不过要使用中括号语法来访问,如下面的例子所示,因为大多数简写形式都不是合法的ECMAScript标识符:

    let text = "this has been a short summer";
    let pattern = /(.)hort/g;
    /*
      * 注意:Opera不支持简写属性名
      * IE不支持多行匹配
      */
    if (pattern.test(text)) {
      console.log(RegExp.$_);         // this has been a short summer
      console.log(RegExp["$`"]);     // this has been a
      console.log(RegExp["$'"]);     // summer
      console.log(RegExp["$&"]);     // short
      console.log(RegExp["$+"]);     // s
    }

RegExp还有其他几个构造函数属性,可以存储最多9个捕获组的匹配项。这些属性通过RegExp.$1~RegExp.$9来访问,分别包含第1~9个捕获组的匹配项。在调用exec()或test()时,这些属性就会被填充,然后就可以像下面这样使用它们:

    let text = "this has been a short summer";
    let pattern = /(..)or(.)/g;
    if (pattern.test(text)) {
      console.log(RegExp.$1);   // sh
      console.log(RegExp.$2);   // t
    }

在这个例子中,模式包含两个捕获组。调用test()搜索字符串之后,因为找到了匹配项所以返回true,而且可以打印出通过RegExp构造函数的$1和$2属性取得的两个捕获组匹配的内容。

注意 RegExp构造函数的所有属性都没有任何Web标准出处,因此不要在生产环境中使用它们。

5.2.4 模式局限

虽然ECMAScript对正则表达式的支持有了长足的进步,但仍然缺少Perl语言中的一些高级特性。下列特性目前还没有得到ECMAScript的支持(想要了解更多信息,可以参考Regular-Expressions.info网站):

❑ \A和\Z锚(分别匹配字符串的开始和末尾)

❑ 联合及交叉类

❑ 原子组

❑ x(忽略空格)匹配模式

❑ 条件式匹配

❑ 正则表达式注释

虽然还有这些局限,但ECMAScript的正则表达式已经非常强大,可以用于大多数模式匹配任务。