JavaScript权威指南--正则表达式

知识要点

正则表达式,是一个描述字符模式的对象。javascript的RegExp类表示正则表达式,String和RegExp都定义了方法,后者使用正则表达式进行强大的模式匹配和文本检索与替换功能。

1.正则表达式的定义

javascript中的正则表达式用RegExp对象表示,可以使用RegExp()构造函数来创建RegExp对象,也可以通过一种特殊的直接语法量一对斜杠(/)来创建。

var patten = /s$/;

运行这段代码创建一个新的RegExp对象,并将它赋值给patten.这个特殊的RegExp对象用来匹配以"s"结尾的字符串。等价于:

var patten = new RegExp( "s$" );

RegExp直接量和对象的创建

就像字符串和数字一样,程序中每个取值相同的原始类型直接量均表示相同的值,这是显而易见的。程序运行时每次遇到对象直接量(初始化表达式)诸如{}和[]的时候都会创建新对象。比如,如果在循环体中写 var a = [],则每次遍历都会创建一个新的空数组。正则表达式直接量与此不同,ECMAscript3规定,一个正则表达式直接量会在执行到它时转化为一个RegExp对象,同一段代码所表示正则表达式直接量的每次运算都返回同一个对象。ECMAScript5做了相反的规定,同一段代码所表示的这种表达式直接量每次运算都返回新对象。IE一直都是按照ECMAScript5规范实现的。

正则表达的模式规则是由一个字符序列组成的。包括所有字母和数字在内,大多数的字符都是按照直接量仅描述待匹配的字符的。除此之外,正则表达式中还有其他具有特殊语义的字符,这些字符并不按照字面含义进行匹配。

1.1.直接量字符

正则表达式中所有字符和数字都是按照字面含义进行匹配的。javascript正则表达式语法也支持非字母的字符匹配,这些字符需要通过反斜线(\)作为前缀进行转转义。

在正则表达式中,许多标点符号有特殊的含义,它们是:^ $ . * + ? = ! : | \ / ( ) [ ] { }

这些符号只有在正则表达式的某些上下文才有特殊含义。在其他上下文中则被当成直接量处理。然而想在正则表达式中使用这些字符的直接量进行匹配,则必须使用前缀\,这是一条通行规则。其他标点符号(比如@和引号)没有特殊含义,在正则表达式中按照字面量含义进行匹配。

如果不记得那些标点符号需要反斜线转义,可以在每个标点符号前都加上反斜线。另外需要注意,许多字面和数字在反斜线前做前缀也有特殊含义,所以,对于想按照直接量进行匹配的字面和数字,尽量不要用反斜线进行转义。当前,想要在正则表达式中按照直接量匹配反斜线本身,则必须使用反斜线将其转义。比如,正则表达式“/\\/”用以匹配任何包含反斜线的字符串。

1.2.字符类

将直接量字符单独放进方括号内就成了字符类(character class)。一个字符类可以匹配它包含的任意字符。比如/[abc]/,另外,可以通过"^"来定义否定字符类。字符类可以使用连字符来表示字符的范围,例如/[a-zA-Z0-9]/。

由于某些字符类非常常用,因此在javascript的正则表达式语法中,使用这些特殊字符的转义来表示它们。例如,\s匹配空格符、制表符和其它Unicode空白符。

注意,有些字符类转义只能匹配ASCII字符,还没有扩展到可以处理Unicode类字符,例如/[\u0400-\u04FF]/用以匹配所有的Cyrillic字符。

注意,在方括号之内也可以写成这些特殊转义字符。比如,由于\s匹配所有的空白字符,\d匹配的是所有数字,因此,/[\s\d]/就是匹配任意空白或数字。这里有一个特例,我们将会看到,转义符\b具有特殊含义,当用在字符类中时,它表示的是退格符,所以要在正则表达式中按照直接量表示一个退格符,只需要使用具有一个元素的字符类/[\b]/ 。

1.3.重复

我们在正则模式之后跟随用以指定字符重复的标记。有一些专门用于表示这种情况的特殊字符。例如:“+”用以匹配前一个或多个副本。下面的表总结了表示重复的正则语法。

/\d{2,4}/         //匹配2~4个数字
/\w{3}\d?/        //精确匹配三个单词和一个可选的数字
/\s+java\s+/      //匹配前后带一个或多个空格的字符串“java”
/[^(]*/           //匹配一个或多个非左括号的字符

在使用"*"和"?"时要注意,由于这些字符可能匹配0个字符,因此它们允许什么都不匹配。例如正则表达式/a*/实际上与字符串"bbbb"匹配,因为这个字符串含有0个a。

非贪婪的重复

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

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

1.4.选择、分组和引用

正则表达式的语法还包括指定选择项、子表达式分组和引用前一子表达式的特殊字符。

选择

字符"|"用于分隔供选择的字符,例如/d{3}|[a-z]{4}/是三位数字或4个小写字母。

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

分组

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

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

引用

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

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

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

/['"][^'"]*['"]/

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

/(['"])[^'"]*\1/

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

/(['"])[^\1]*\1/

对圆括号的子表达式的引用,是正则表达式检索和替换操作的强大特性之一。

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

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

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

1.5.指定匹配的位置

正则表达式中的多个元素才能够匹配字符串的一个字符。例如,\s匹配的只是一个空白符 。还有一些正则表达式的元素匹配的是字符之间的位置,而不是实际的字符,例如,\b匹配一个单词的边界。像\b这样的元素不匹配某个可见的字符,它们指定匹配发生的合法位置。有时,我们称这些元素为正则表达式的锚,因为他们将模式定位在搜索字符串的特定位置上。最常用的锚元素是^,它用来匹配字符串的开始,锚元素$用来匹配字符串的结束。

例如,要匹配单词“JavaScirpt”,可以使用正则表达式/^JavaScript$/。如果想匹配"Java"这个单词本身(不像在“JavaScript”做单词的前缀),可以使用/\s\Javas/,可以匹配前后都有空格的单词“Java”。但是这样做有两个问题,第一。如果"Java"出现在字符串的开始或者结尾,就匹配不成功,除非开始和结尾处各有一个空格。第二个问题是,当找到了与之匹配的字符串时,它返回的匹配字符串的前端和后端都有空格。因此,我们使用单词的边界\b来代替真正的空格符\s进行匹配(或定位)。这样的正则表达式就写成了/\bJava\b/。元素\B把匹配的锚点定位在不是单词的边界之处。因此,正则表达式/\B[Ss]cript/与"JavaScript"和"postscript"匹配,但不予"script"和“Scripting”匹配。

任意正则表达式都可以作为锚点条件。如果在符号"(?="和")"之间加入一个表达式,它就是一个先行断言,用以说明圆括号内的表达式必须正确匹配,但不是真正意义上的匹配,可以使用/[Jj]ava([Ss]cript)?(?=\)/。这个正则表达式可以匹配“Jvascript: The Definitive Guide”中的javascript,但不能匹配"Java in NutShell"中的"Java",因为它后面没有冒号。

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

1.6.修饰符

正则表达式的修饰符,用以说明高级匹配模式的规则。和之前讨论的正则表达式语法不同,修饰符是放在"/"符号之外的。Javascript支持三个修饰符:i/m/g。比如,/java$/im,可以匹配java,也可以匹配"Java\nis Fun"。

2.用于模式匹配的String方法

如何在javascript使用这些正则表达式。本节将讨论String对象的一些用以执行正则表达式模式匹配和检索替换操作的方法,后续几节还会讨论如何使用javascript正则表达式的模式匹配,不过将侧重于RegExp的对象和它的方法及属性。可以书中第三部分查找详细介绍。

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

search()

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

"javascript".search(/script/i)

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

replace()

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

text.replace(/javascript/gi,"JavaScript");

但replace()的功能远不至于如此。正则表达式中使用的圆括号括起来的子表达式是带有从左到右的索引编号的 ,而且正则表达式会记忆与每个子表达式匹配的文本。如果在替换字符串中出现了这两个字符串。这是一个非常有用的特性。比如,可以用它将一个字符串中的英文引号替换为中文半引号。

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

replace()方法还有一些其他的重要特性,这些特性将在第三部分的String.replace()的主题页中进行介绍。最值得注意的是,replace()方法的第二个参数可以是函数,该函数能够动态地计算替换字符串。

match()

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

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

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

例如,使用如下的代码来解析一个URL:

var url = /(\w+):\/\/([\w.]+)\/(\S*)/;
var text = "Visit my blog at http://www.example.com/~david";
var result = text.match(url);
if(result != null){
    var fullurl = result[0];
    var protocol = result[1];
    var host = result[2];
    var path = result[3];
}

值得注意的是,给字符串的match()方法传入一个非全局的正则表达式,实际上和给这个正则表达式exec()方法传入的字符串是一模一样的,它返回的数组带有两个属性:index和input,接下来exec()方法的讨论中会提到:

split()

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

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

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

"1, 2, 3, 4, 5".split(/\s*,\S*/) //=> ["1", " 2", " 3", " 4", " 5"]

书中第三部分有详情。

3.RegExp对象

正则表达式是通过RegExp独享来表示的,除了RegExp()构造函数之外,RegExp对象还支持三个方法和一些属性。

RegExp()构造函数带有两个字符串参数,其中第二个参数是可选的,RegExp()用以创建新的RegExp对象。第一个参数包含正则表达式的主体部分,也就是正则表达式直接量中两条斜线之间的文本。需要注意的是,不论是字符串直接量还是正则表达式,都使用“\”字符作为转义字符的前缀,因此当给RegExp()传入一个字符串表述的正则表达式时,必须将“\”替换成“\”。RegExp()的第二个参数是可选的,如果提供第二个参数,它就指定正则表达式的修饰符。不过只能传入修饰符g、i、m或者它们的组合。比如:

// 全局匹配字符串中的5个数字,注意这里使用了"\\",而不是"\"
var zipcode = new RegExp("\\d{5}", "g");

RegExp()构造函数在需要动态创建正则表达式的时候非常有用,这种情况无法通过直接量实现。例如,如果待检索的字符串是由用户输入的,就必须使用RegExp()构造函数,在程序运行时创建正则表达式。

3.1 RegExp的属性

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

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

3.2 RegExp的方法

RegExp对象定义了两个用于执行模式匹配操作的方法。它们的行为和上文介绍过的String方法很类似。RegExp最主要的执行模式匹配的方法是exec(),它与10.2节介绍过的String方法match()相似,只是RegExp方法的参数是一个字符串,而String方法的参数是一个RegExp对象。

exec()

该方法对一个指定的字符串执行一个正则表达式,简而言之,就是在一个字符串中执行匹配检索。如果没有找到任何匹配,它将返回 null ,但如果它找到了一个匹配,将返回一个数组,就像方法 match() 为非全局检索返回的数组一样。这个数组的第一个元素包含的是与正则表达式相匹配的字符串,余下的所有元素包含的是与圆括号内的子表达式相匹配的子串。而且,属性 index 包含了匹配发生的字符的位置,属性 input 引用的是被检索的字符串。

和 match() 方法不同的是,无论该正则表达式是否具有全局标记 g,exec() 都会返回一样的数组 。回忆一下,当 match() 的参数是一个全局正则表达式时,它返回由匹配结果组成的数组。相比之下,方法 exec() 返回的总是一个匹配结果,而且提供关于本次匹配的完整信息。当一个具有 g 标志的正则表达式调用 exec() 时,它将把当前正则表达式对象的 lastIndex 属性设置到紧挨着匹配子串的字符位置。当同一个正则表达式第二次调用exec() 时,它将从 lastIndex 属性所指示的字符处开始检索。如果 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);
}
var pattern = /Java/g;
var text = "JavaScript is more fun than Java!";
var result = pattern.exec(text)
console.log(result)   //["Java"]

 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 属性自动重置为 0,要么显示将lastIndex 手动设置为0 (当最后一次检索失败时需要手动设置lastIndex ) 。如果忘记了手动设置lastIndex的值,那么下一次对新字符串进行检索时,执行检索的起始位置可能就不是字符串的开始位置,而可能是任意位置。当然,如果RegExp不带有修饰符g,则不必担心会发生这种情况。同样要记住,在ES5中,正则表达式直接量的每次计算都会创建一个新的RegExp对象,每个新RegExp对象具有各自的lastIndex属性,这势必会大大减少"残留"lastIndex对程序造成的意外影响。

 

20170420补充

RegExp.$1...$9属性用于返回正则表达式模式中某个子表达式匹配的文本。

正则表达式中每个小括号内的部分表达式就是一个子表达式。该属性是RegExp全局对象的一个只读属性,所有主流浏览器均支持该属性。

RegExp.$1...$9属性的值为String类型,返回上一次正则表达式匹配中,第n个子表达式所匹配的文本。虽然正则表达式模式中的小括号可以有任意多个,但是此属性只保存最前面的9个匹配文本。

注意:这里的RegExp是全局对象,RegExp.$1...$9是全局属性。当执行任意正则表达式匹配操作时,JavaScript会自动更新全局对象RegExp上的全局属性,用以存储此次正则表达式模式的匹配结果。当再次执行正则表达式匹配时,RegExp上的全局属性又会更新,覆盖掉之前的存储数据,以反映本次正则表达式模式的匹配结果。

var str = "X98Y87Z65";
// 三个数字部分加了小括号,表示子表达式
var reg = /^X(\d+)Y(\d+)Z(\d+)$/;
reg.test(str); // 此处使用exec()等其他正则表达式的匹配方法也可,下同
document.writeln(RegExp.$1); // 98
document.writeln(RegExp.$2); // 87
document.writeln(RegExp.$3); // 65

str = '<a href="http://www.365mini.com/" title="首页" target="_blank">CodePlayer</a>';
// 提取URL
reg = /<a\s[^>]*href="([^">]+)"[^>]*>/;
reg.exec(str);
document.writeln(RegExp.$1); // http://www.365mini.com/

 

posted @ 2017-03-21 11:33  chenxj  阅读(790)  评论(0编辑  收藏  举报