正则表达式 (regular expression) 是一个描述字符模式的对象。JavaScript 的 RegExp 类表示正则表达式,而 String 和 RegExp 都定义了使用正则表达式进行强大的模式匹配和文本检索与替换的函数。
ECMAScript v3 对 JavaScript 正则表达式进行了标准化。JavaScript 1.2 实现了 ECMAscript v3 要求的正则表达式特性的子集,JavaScript 1.5 实现了完整的标准。JavaScript 的正则表达式完全以 Perl 程序设计语言的正则表达式工具为基础。粗略地说, JavaScript 1.2 实现了 Perl 4 的正则表达式,JavaScript 1.5 实现了 Perl 5 的正则表达式的大型子集。
本章定义了正则表达式用来描述文本模式的语法。它还介绍了使用正则表达式的 String 与 RegExp 方法。
在 JavaScript 中,正则表达式由 RegExp 对象表示。当然,可以使用 RegExp() 构造函数创建 RegExp 对象,不过通常还是用特殊的直接量语法来创建 RegExp 对象。就像字符串直接量被定义为包含在引号内的字符一样,正则表达式直接量也被定义为包含在一对斜杠 (/) 之间的字符。所以,JavaScript 可能会包含如下的代码:
var pattern = /s$/;
这行代码创建了一个新的 RegExp 对象,并且将它赋给了变量 pattern 。这个特殊的 RegExp 对象和所有的以字母 “s” 结尾的字符串都匹配。用构造函数 RegExp() 也可以定义一个等价的正则表达式,其代码如下:
var pattern = new RegExp("s$");
无论使用正则表达式直接量还是用构造函数 RegExp() ,创建一个 RegExp 对象都比较容易。较为困难的是用正则表达式语法来描述所需的字符的模式。JavaScript 采用的是 Perl 语言使用的正则表达式语法的相当完整的子集,因此,如果读者是一个经验丰富的 Perl 程序员,那么就会知道在 JavaScript 中如何描述模式。
正则表达式的模式规范是由一系列字符构成的。大多数的字符 (包含所有的字母数字字符) 描述的都是按照直接量进行匹配的字符。这样说来,正则表达式 /java/就和所有的包含子串 "java" 的字符串相匹配。虽然正则表达式中的其他字符不是按照直接量进行匹配的,但是它们都具有特殊的意义。例如,正则表达式 /s$/ 包含两个字符。第一个字符 "s" 按照直接量与自身相匹配。第二个字符 "$" 是一个特殊元字符,它所匹配的是字符串的结尾。所以正则表达式 /s$/ 匹配的就是以字母 "s" 结尾的字符串。
接下来的几节介绍了用于 JavaScript 正则表达式的各种字符和元字符。但是注意,有关正则表达式语法的完整介绍远远超出了本书的范围。
正如前面所提到的,在正则表达式中所有的字母字符和数字都是按照直接量与自身相匹配的。JavaScript 的正则表达式语法还通过以反斜杠 (\) 开头的转义序列支持某些非字母的字符。例如,序列 "\n" 在字符串中匹配的是直接量换行符。下表列出了这些字符。
在正则表达式中,许多标点符合具有特殊的含义。它们是:
在接下来的几节中,我们将学习这些符号的含义。某些符号只在正则表达式的特殊环境中才具有特殊的含义,在其他环境中则被按照直接量进行处理。但是,作为一个通用的原则,如果在正则表达式中按照直接量使用这些标点符合,就必须加前缀 \ 。其他标点符号 (如引号和 @) 没有特殊含义,在正则表达式中只按照直接量匹配自身。
将单独的直接量字符放进方括号内就可以组合成字符类 (character class)。一个字符类和它所包含的任何字符都匹配。所以正则表达式 /[abc]/ 就和字母 "a" ,"b" ,"c" 中的任何一个字母都匹配。另外,还可以定义否定字符类,这些类匹配的是不包含在方括号之内的所有字符。定义否定字符类的时候,要将一个 ^ 符合作为左方括号后的第一个字符。正则表达式 /[^abc]/ 匹配的是 "a" 、“b” 、"c" 之外的所有字符。字符类可以使用连字符来表示一个字符范围。要匹配拉丁字母集中的任何小写字符,可以使用 /[a-z]/,要匹配拉丁字母集中任何字母数字字符,则使用 /[a-zA-Z0-9]/ 。
由于某些字符类非常常用,所以 JavaScript 的正则表达式语法就包含了一些特殊字符和转义序列来表示这些常用的类。例如, \s 匹配的是空格符、制表符和其他 Unicode 空白符,\S 匹配的是非 Unicode 空白符的字符。下表列出了这些字符,并且总结了字符类的语法。(注意,有些字符类转义序列只匹配 ASCII 字符,还没有扩展到可以处理 Unicode 字符。可以显式地定义自己的 Unicode 字符类,例如, /[\u0400-\u04FF]/ 匹配所有的 Cyrillic 字符)。
注意,在方括号之内也可以使用这些特殊的字符转义序列。例如 \s 匹配的是所有的空白符,\d 匹配的是所有数字,那么 /[\s\d]/ 就匹配任意的空白符或数字。注意,这里有一个特例。下面我们将会看到转义序列 \b 具有特殊含义,当用在字符类中时,它表示的是退格符,所以要在正则表达式中按照直接量表示一个退格符,只需要使用具有一个元素的字符类/[\b]/。
用刚刚学过的正则表达式的语法,可以把两位数描述成 /\d\d/ ,把四位数描述成 /\d\d\d\d/ 。但是还没有一种方法可以用来描述具有任意多数位的数字,或描述字符串,这个字符串由三个字母以及跟随在字母之后的一位数字构成。这些较复杂的模式使用的正则表达式语法都指定了该正则表达式中的一个元素重复出现的次数。
指定重复的字符总是出现在它们所作用的模式之后。由于某种重复类型相当常用,所有有一些特殊的字符专门用于表示这种情况。例如,+号匹配前一模式的一个或多个副本。下表总结了这些重复的语法。
下面的代码是一些例子:
/\d{2,4}/ // 匹配 2 到 4 个数字
/\w{3}\d?/ // 匹配 3 个字符 和 0 或 1个数字
/\s+java\s+/ // 匹配 "java" 前后有 1 个或多个空白符
/[^"]*/ // 匹配 0或多个不是 " 的字符
在使用重复字符 * 和 ? 时要注意,由于这些字符可能匹配前面字符的 0 个实例,所以它们允许什么都不匹配。例如,正则表达式 /a*/ 实际上与字符串 "bbbb" 匹配,因为这个字符串含有 0个 a 。
非贪婪的重复
上表中列出的重复字符可以匹配尽可能多的字符,而且允许接下来的正则表达式继续匹配。因此,我们说重复是 "贪婪的" 。可以以非贪婪的方式进行重复(在 JavaScript 1.5 和其后的版本中可以做到,这是 Perl 5 的一个特性,JavaScript 1.2 没有实现它),只需要在重复字符后加问号即可 (如 ?? 、+? 、*? ,甚至 {1,5}?)。例如,正则表达式 /a+/ 匹配一个或多个字符 a 。将其应用到字符串 "aaa" 上时,它与三个字母都匹配。但是 /a+?/只匹配一个或多个必要的字母 a 。将其应用到同样的字符串上时,该模式只匹配第一个字母 a 。
使用非贪婪的重复生成的结果并不总是与期望一致。考虑模式 /a*b/ ,它匹配 0个或多个字母 a 后跟随字母 b 。在应用到字符串 “aaab” 上时,它匹配整个字符串。现在使用非贪婪的重复版本 /a*?b/ ,它应该匹配字母b ,通过在字母 b 前加最少的字母 a 。在应用到同一个字符串 "aaab" 上时,读者可能以为它只匹配最后一个字母 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*)/
对正则表达式中前一子表达式的引用所指的并不是那个子表达式的模式,而是与那个模式相匹配的文本。这样,引用可以用于实施一条约束,即一个字符串各个单独的部分包含的是完全相同的字符。例如,下面的正则表达式匹配的就是位于单引号或双引号之内的0个或多个字符。但是,它并不要求开始和结束的引号匹配 (例如两个都是双引号或单引号):
/[' "][^' "]*[' "]/
如果要求开始和结束的引号相匹配,可以使用如下的引用:
/([' "])[^' "]*\1/
\1 匹配的是第一个带括号的子表达式所匹配的模式。在这个例子中,它实施了一条约束,那就是开始的引号必须和结束的引号相匹配。正则表达式不允许用双引号括起的字符串中有单引号,反之亦然。在字符类中使用引用是不合法的,所以我们不能编写:
/([' "])[^\1]*\1/
在本章后面的小节中,我们会看到一种对带括号的子表达式的引用,它们是对正则表达式进行检索和替换操作的强大特性之一。
在 JavaScript 1.5 (不是 JavaScript 1.2) 中,无须创建带编码的引用就可以将正则表达式中的项目进行组合。它不是以 "(" 和 ")" 对项目进行分组,而以 "(?:" 和 ")" 来分组。考虑如下的模式,例如:
/([Jj]ava(?:[Ss]cript)?)\sis\s(fun\w*)/
这里,子表达式 (?:[Ss]cript) 仅仅用于分组,因此复制符合 "?" 可以应用到各个分组。这种改进了的括号并不生成引用,所以在这个正则表达式中,\2 引用了与 (fun\w*) 匹配的文本。
下表总结了正则表达式的选择、分组和引用运算符。
正如前面所介绍的,正则表达式中的多个元素才能够匹配字符串中的一个字符。例如 \s 匹配的只是一个空白符。还有一些正则表达式的元素匹配的是字符之间的位置,而不是实际字符。例如 \b 匹配的是一个单字的边界,即位于 \w (ASCII 单字字符) 字符和 \W (非单字字符) 之间的边界,或位于一个 ASCII 单字字符与一个字符串的开头或结尾之间的边界。像 \b 这样的元素不指定匹配的字符串中使用的字符,它们指定的是匹配所发生的合法位置。有时我们称这些元素为正则表达式的锚,因为它们将模式定位在检索字符串中的一个特定位置上。最常用的锚元素是 ^ ,它使模式定位在字符串的开头,而锚元素 $ 则使模式定位在字符串的末尾。
例如,要匹配单字 "JavaScript" ,可以使用正则表达式 /^JavaScript$/ 。如果想检索 "Java" 这个单字自身 (不像在 "JavaScript" 中那样作为前缀),可以使用模式 /\sJava\s/ ,它要求在单字 Java 之前和之后都要有空格。但是这样做有两个问题。第一,如果 "Java" 出现在一个字符串的开头或结尾,该模式就会不与之匹配,除非在开头处或者结尾处有一个空格。第二个问题是,当这个模式找到了一个与之匹配的字符串时,它返回的匹配字符串的前端和后端都有空格,这并不是我们想要的。因此,我们使用单字的边界 \b 来代替真正的空格符 \s 进行匹配。结果表达式是 /\bJava\b/ 。元素 \B 将把匹配锚定在不是单字边界的位置。因此,模式 /\B[Ss]cript/ 与 "JavaScript" 和 "postscript" 匹配,但是不与 "script" 和 "Scripting" 匹配。
在 JavaScript 1.5 中 (不是 JavaScript 1.2 ) ,还可以使用任意的正则表达式作为锚定条件。如果在符合 "(?=" 和 ")" 之间加入一个表达式,它就是一个向前声明,指定接下来的字符必须被匹配,但并不真正进行匹配。例如,要匹配一种常用的程序设计语言的名字,但只在其后有冒号时匹配,可以使用 /[Jj]ava([Ss]cript)?(?=\:)/ 。这个模式与 "JavaScript: The Definitive Guide" 中的单字 "JavaScript" 匹配,但是与 "Java in a Nutshell" 中的 "Java" 不匹配,因为其后没有冒号。
如果用 "(?!" 引入声明,它将是反前向声明,指定接下来的字符都不必匹配。例如,/Java(?!Script)([A-Z]\w*)/ 匹配的是 "Java" 后跟随一个大写字母和任意多个 ASCII 单字字符,但是不能跟随 "Script" 。它与 "JavaBeans" 匹配,不与 "Javanese" 匹配,与 "JavaScrip" 匹配,但不与 "JavaScript" 或 "JavaScripter" 匹配。
下表总结了正则表达式的锚
正则表达式的语法还有最后一个元素,即正则表达式的标志,它说明高级模式匹配的规则。和其他的正则表达式语法不同,标志是在 "/" 符号之外说明的,即它们不出现在两个斜杠之间,而是位于第二个斜杠之后。JavaScript 1.2支持两个标志。标志 i 说明模式匹配不区分大小写。标志 g 说明模式匹配应该是全局的,也就是说,应该找出被检索的字符串中所有的匹配。这两种标志联合起啦就可以执行一个全局的不区分大小写的匹配。
例如,要执行一个不区分大小写的检索以找到单字 "java" (或者 “Java” 、"JAVA" 等) 的第一次出现,可以使用不区分大小写的正则表达式 /\bjava\b/i 。如果要在一个字符串中找到所有出现的 "java" ,需要添加标志 g ,即 /\bjava\b/gi 。
JavaScript 1.5 还支持一个标志 m ,它以多行模式执行模式匹配。在这种模式中,如果要检索的字符串中含有换行符, ^ 和 $ 锚除了匹配字符串的开头和结尾外还匹配每行的开头和结尾。例如,模式 /Java$/im 匹配 "java" 和 "Java\nis fun" 。
下表总结了这些正则表达式的标志。注意,我们在本章后面的小节中介绍用于实际执行匹配的 String 和 RegExp 方法时还将看到更多有关标志 g 的用法。