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的正则表达式已经非常强大,可以用于大多数模式匹配任务。