JavaScript正则表达式的匹配模式

zzrshuiwuhen 2019-06-21

正则表达式(regular expression)是一个描述字符模式的对象。JavaScript的 RegExp类 表示正则表达式,String和RegExp都定义了方法,后者使用正则表达式进 行强大的模式匹配文本检索与替换功能。JavaScript的正则表达式语法是Perl5的正则表达式语法的大型子集,所以对于有Perl编程经验的程序员来说,学习JavaScript 中的正则表达式[1]是小菜一碟。

定义

JavaScript中的正则表达式用 RegExp对象 表示,可以使用RegExp()构造函数来创 建RegExp对象,不过RegExp对象更多的是通过一种特殊的直接量语法来创建。就像通过引号包裹字符的方式来定义字符串直接量一样,正则表达式直接量定义为包含在一对斜杠 (/) 之间的字符,例如:

var pattern=/s$/;

运行这段代码创建一个新的RegExp对象,并将它赋值给变量pattern。这个特殊 的RegExp对象用来匹配所有以字母“s”结尾的字符串。用构造函数RegExp()也可以 定义个与之等价的正则表达式,代码如下:

var pattern=new RegExp(“s$”);

正则表达式直接量则与此不同,ECMAScript 3规范规定,一个正则表达式直接 量会在执行到它时转换为一个RegExp对象,同一段代码所表示正则表达式直接量的 每次运算都返回同一个对象。ECMAScript 5规范则做了相反的规定,同一段代码所 表示的正则表达式直接量的每次运算都返回新对象。IE一直都是按照ECMAScript 5 规范实现的,多数最新版本的浏览器也开始遵循ECMAScript 5。

高级语法

非贪婪的重复

一般匹配重复字符是尽可能多地匹配,而且允许后续的正则表达式 继续匹配。因此,我们称之为“贪婪的”匹配。我们同样可以使用正则表达式进行非贪婪匹配。只须在待匹配的字符后跟随一个问号即 可:“??”、“+?”、“*?”或“{1,5}?”。比如,正则表达式/a+/可以匹配一个或多个连续 的字母a。当使用“aaa”作为匹配字符串时,正则表达式会匹配它的三个字符。但 是/a+?/也可以匹配一个或多个连续字母a,但它是尽可能少地匹配。我们同样 将“aaa”作为匹配字符串,但后一个模式只能匹配第一个a。

使用非贪婪的匹配模式所得到的结果可能和期望并不一致。考虑以下正则表达 式/a+b/,它可以匹配一个或多个a,以及一个b。当使用“aaab”作为匹配字符串时, 它会匹配整个字符串。现在再试一下非贪婪匹配的版本/a+?b/,它匹配尽可能少的a 和一个b。当用它来匹配“aaab”时,你期望它能匹配一个a和最后一个b。但实际上, 这个模式却匹配了整个字符串,和该模式的贪婪匹配一模一样。这是因为正则表达 式的模式匹配总是会寻找字符串中第一个可能匹配的位置。由于该匹配是从字符串的第一个字符开始的,因此在这里不考虑它的子串中更短的匹配。

选择、分组和引用

正则表达式的语法还包括指定选择项、子表达式分组引用前一子表达式的特殊字符。字符“|”用于分隔供选择的字符。例如,/ab|cd|ef/ 可以匹配字符串“ab”,也 可以匹配字符串“cd”,还可以匹配字符串“ef”。/d{3}|[a-z]{4}/匹配的是三位数字或 者四个小写字母。

注意,选择项的尝试匹配次序是从左到右,直到发现了匹配项。如果左边的选择项匹配,就忽略右边的匹配项,即使它产生更好的匹配。因此,当正则表达式 /a|ab/匹配字符串“ab”时,它只能匹配第一个字符。

正则表达式中的圆括号有多种作用。一个作用是把单独的项组合成子表达式, 以便可以像处理一个独立的单元那样用“|”、“*”、“+”或者“?”等来对单元内的项进行处理。例如, /java(script)?/ 可以匹配字符串“java”,其后可以有“script”也可以没有。/(ab|cd)+|ef/ 可以匹配字符串“ef”,也可以匹配字符串“ab”或“cd”的一次或多次重复。

在正则表达式中,圆括号的另一个作用是在完整的模式中定义子模式。当一个正则表达式成功地和目标字符串相匹配时,可以从目标串中抽出和圆括号中的子模式相匹配的部分。例如,假定我们正在检索的模式是一个或多个小写字母后面跟随了一位或多位数字, 则可以使用模式 /[a-z]+\d+/。但假定我们真正关心的是每个匹配尾部的数字,那么如果将模式的数字部分放在括号中 (/[a-z]+(\d+)/) ,就可以从检索到的匹配中抽取数字 了,之后我们会有详尽的解释。

带圆括号的表达式的另一个用途是允许在同一正则表达式的后部引用前面的子表达式。这是通过在字符 “\” 后加一位或多位数字来实现的。这个数字指定了带圆括 号的子表达式在正则表达式中的位置。例如,\1引用的是第一个带圆括号的子表达 式,\3 引用的是第三个带圆括号的子表达式。注意,因为子表达式可以嵌套另一个子表达式,所以它的位置是参与计数的左括号的位置。例如,在下面的正则表达式 中,嵌套的子表达式 ([Ss]cript) 可以用 \2来指代:

/([Jj]ava([Ss]cript)?)\sis\s(fun\w*)/

JavaScript正则表达式的匹配模式

对正则表达式中前一个子表达式的引用,并不是指对子表达式模式的引用,而指的是与那个模式相匹配的文本的引用。这样,引用可以用于实施一条约束,即一个字符串各个单独部分包含的是完全相同的字符。例如,下面的正则表达式匹配的就是位于单引号或双引号之内的0个或多个字符。但是,它并不要求左侧和右侧的引号匹配(即,加入的两个引号都是单引号或都是双引号):

/[’”][^’”]*[’”]/

如果要匹配左侧和右侧的引号,可以使用如下的引用:

/([’”])[^’”]*\1/

\1 匹配的是第一个带圆括号的子表达式所匹配的模式。在这个例子中,存在这样一条约束,那就是左侧的引号必须和右侧的引号相匹配。正则表达式不允许用双引号括起的内容中有单引号,反之亦然。不能在字符类中使用这种引用,所以下面的写法是非法的:

/([’”])[^\1]*\1/

同样,在正则表达式中不用创建带数字编码的引用,也可以对子表达式进行分组。它不是以“(”和“)”进行分组,而是以“(?:”和“)”来进行分组,比如,考虑下面这 个模式:

/([Jj]ava(?:[Ss]cript)?)\sis\s(fun\w*)/

这里,子表达式 (?:[Ss]cript) 仅仅用于分组,因此复制符号”?”可以应用到各个分 组。这种改进的圆括号并不生成引用,所以在这个正则表达式中,\2 引用了与 (fun\W*) 匹配的文本。

下图对正则表达式的选择、分组和引用运算符做了总结:

JavaScript正则表达式的匹配模式

先行断言

任意正则表达式都可以作为锚点条件。如果在符号 “(?=”和“)” 之间加入一个表 达式,它就是一个 先行断言 ,用以说明圆括号内的表达式必须正确匹配,但并不是真正意义上的匹配。比如,要匹配一种常用的程序设计语言的名字,但只在其后 有冒号时才匹配,可以使用 /[Jj]ava([Ss]cript)?(?=\:)/。这个正则表达式可以匹 配“JavaScript:The Definitive Guide”中的“JavaScript”,但是不能匹配“Java in a Nutshell”中的“Java”,因为它后面没有冒号。

JavaScript正则表达式的匹配模式

带有 “(?!” 的断言是负向先行断言,用以指定接下来的字符都不必匹配。例 如, /Java(?!Script)([A-Z]\w*)/可以匹配“Java”后跟随一个大写字母和任意多个ASCII 单词,但Java后面不能跟随“Script”。它可以匹配“JavaBeans”,但不能匹配“Javanese”;它不匹配“JavaScript”,也不能匹配“JavaScripter”。

JavaScript正则表达式的匹配模式

用于模式匹配的String方法

String支持4种使用正则表达式的方法。

search

最简单的是 search()。它的参数是一个正 则表达式,返回第一个与之匹配的子串的起始位置,如果找不到匹配的子串,它将 返回-1。比如,下面的调用返回值为4:

“JavaScript”.search(/script/i);

如果search()的参数不是正则表达式,则首先会通过RegExp构造函数将它转换 成正则表达式, search()方法不支持全局检索,因为它忽略正则表达式参数中的修饰符g。

replace

replace()方法用以执行检索与替换操作。其中第一个参数是一个正则表达式, 第二个参数是要进行替换的字符串。这个方法会对调用它的字符串进行检索,使用指定的模式来匹配。如果正则表达式中设置了修饰符g,那么源字符串中所有与模式匹配的子串都将替换成第二个参数指定的字符串;如果不带修饰符g,则只替换所匹配的第一个子串。如果replace()的第一个参数是字符串而不是正则表达式,则 replace()将直接搜索这个字符串,而不是像search()一样首先通过RegExp()将它转换为正则表达式。比如,可以使用下面的方法,利用replace()将文本中的所有 javascript(不区分大小写)统一替换为“JavaScript”:

//将所有不区分大小写的javascript都替换成大小写正确的JavaScript 
text.replace(/javascript/gi,“JavaScript”);

replace() 的功能远不止这些。回忆一下前文所提到的,正则表达式中使用圆 括号括起来的子表达式是带有从左到右的索引编号的,而且正则表达式会记忆与每 个子表达式匹配的文本。如果在替换字符串中出现了$加数字,那么 replace() 将用与 指定的子表达式相匹配的文本来替换这两个字符。这是一个非常有用的特性。比 如,可以用它将一个字符串中的英文引号替换为中文半角引号:

//一段引用文本起始于引号,结束于引号
//中间的内容区域不能包含引号
var quote=/”([^”]*)”/g;//用中文半角引号替换英文引号,同时要保持引号之间的内容(存储在$1中)没有被修改 
text.replace(quote,’“$1”’);

最值得注意的是,replace()方法的第二个参数 可以是函数,该函数能够动态地计算替换字符串。

match

match() 方法是最常用的String正则表达式方法。它的唯一参数就是一个正则表达式(或通过RegExp()构造函数将其转换为正则表达式),返回的是一个由匹配结 果组成的数组。如果该正则表达式设置了修饰符g,则该方法返回的数组包含字符 串中的所有匹配结果。例如:

“1 plus 2 equals 3”.match(/\d+/g)//返回[“1”,“2”,“3”]

如果这个正则表达式没有设置修饰符 gmatch() 就不会进行全局检索,它只检 索第一个匹配。但即使 match() 执行的不是全局检索,它也返回一个数组。在这种情 况下,数组的第一个元素就是匹配的字符串,余下的元素则是正则表达式中用圆括 号括起来的子表达式。因此,如果 match() 返回一个数组a,那么 a[0] 存放的是完整的 匹配,a[1]存放的则是与第一个用圆括号括起来的表达式相匹配的子串,以此类 推。为了和方法 replace() 保持一致, a[n] 存放的是 $n的内容。

split

split() 这个方法用以将调用 它的字符串拆分为一个子串组成的数组,使用的分隔符是split()的参数,例如:

“123,456,789”.split(“,”);//返回[“123”,“456”,“789”]

split() 方法的参数也可以是一个正则表达式,这使得 split() 方法异常强大。例如,可以指定分隔符,允许两边可以留有任意多的空白符:

“1,2,3,4,5”.split(/\s*,\s*/);//返回[“1”,“2”,“3”,“4”,“5”]

RegExp对象

RegExp的属性

每个RegExp对象都包含5个属性。

  • 属性source是一个只读的字符串,包含正则表达式的文本。
  • 属性global是一个只读的布尔值,用以说明这个正则表达式是否带有修饰符 g
  • 属性 ignoreCase 也是一个只读的布尔值,用以说明正则表达式是否带有修饰符 i
  • 属性 multiline 是一个只读的布尔值,用以说明正则表达式是否带有修饰符 m
  • 最后一个属性 lastIndex,它是一个可读/写的整数。如果匹配模式带有g修饰符, 这个属性存储在整个字符串中下一次检索的开始位置,这个属性会被 exec()test() 方法用到,下面会讲到。

RegExp的方法

RegExp对象定义了两个用于执行模式匹配操作的方法。

exec

RegExp最主要的执行模式匹配的方法是 exec()exec()方法对一个指定的字符串执行一个正则表达 式,简言之,就是在一个字符串中执行匹配检索。如果它没有找到任何匹配,它就 返回null,但如果它找到了一个匹配,它将返回一个数组,就像match() 方法为非全局检索返回的数组一样。

match() 方法不同,不管正则表达式是否具有全局修饰符g, exec()都会返回一样的数组。当调用exec()的正则表达式对象具有修饰符g时,它将把当前正则表达式 对象的lastIndex属性设置为紧挨着匹配子串的字符位置。当同一个正则表达式第二 次调用 exec() 时,它将从 lastIndex 属性所指示的字符处开始检索。

var pattern = /Java/g;
var text = "JavaScript is more fun than Java!";
result = pattern.exec(text)
//结果
//["Java", index: 0, input: "JavaScript is more fun than Java!"]
pattern.lastIndex //4

如果 exec() 没有发现任何匹配结果,它会将 lastIndex 重置为0(在任何时候都可以将 lastIndex属性设置 为0,每当在字符串中找最后一个匹配项后,在使用这个RegExp对象开始新的字符串查找之前,都应当将 lastIndex 设置为0)。这种特殊的行为使我们可以在用正则表达式匹配字符串的过程中反复调用 exec(),比如:

var pattern = /Java/g;
var text = "JavaScript is more fun than Java!";
var result;
while ((result = pattern.exec(text)) != null) {
  alert("Matched '" + result[0] + "'" +
    " at position " + result.index +
    "; next search begins at " + pattern.lastIndex);
}

test

另外一个RegExp方法是 test() ,它比 exec() 更简单一些。它的参数是一个字符串,用 test() 对某个字符串进行检测,如果包含正则表达式的一个匹配结果,则返回 true :

var pattern=/java/i; 
pattern.test(“JavaScript”);//返回true

调用 test() 和调用 exec() 等价,当 exec() 的返回结果不是null时,test()返回true。由于这种等价性,当一个全局正则表达式调用方法 test() 时,它的行为和 exec() 相同, 因为它从 lastIndex 指定的位置处开始检索某个字符串,如果它找到了一个匹配结果,那么它就立即设置 lastIndex 为当前匹配子串的结束位置。这样一来,就可以使用test() 来遍历字符串,就像用 exec() 方法一样。

exec()test() 不同, String方法 search()replace()match() 并不会用到 lastIndex 属性。实际上, String 方法只是简单地将 lastIndex 属性值重置为0。如果让一 个带有修饰符 g 的正则表达式对多个字符串执行 exec()test(),要么在每个字符串中 找出所有的匹配以便将 lastIndex 自动重置为零,要么显式将 lastIndex 手动设置为 0(当最后一次检索失败时需要手动设置 lastIndex )。

如果忘了手动设置 lastIndex 的值,那么下一次对新字符串进行检索时,执行检索的起始位置可能就不是字符串的 开始位置,而可能是任意位置。当然,如果RegExp不带有修饰符g,则不必担心会发生这种情况。同样要记住,ECMAScript 5中,正则表达式直接量的每次计算都会创建一个新的 RegExp对象,每个新 RegExp对象 具有各自的 lastIndex属性,这势必会大大减少“残留” lastIndex 对程序造成的意外影响。

参考

相关推荐