本章概述了JavaScript 正则表达式 API。它假设您大致熟悉它们的工作原理。如果您不熟悉,网络上有许多很好的教程。以下是两个例子:
此处使用的术语密切反映了 ECMAScript 规范中的语法。我有时会偏离规范,以便于理解。
\ ^ $ . * + ? ( ) [ ] { } |
您可以通过在前面加上反斜杠来转义它们。例如
> /^(ab)$/.test('(ab)') false > /^\(ab\)$/.test('(ab)') true
其他特殊字符是
在字符类 [...]
中
-
在以问号开头的组中 (?...)
: = ! < >
尖括号仅由 XRegExp 库使用(请参阅第 30 章)来命名组。
.
(点)匹配任何 JavaScript 字符(UTF-16 代码单元),但行终止符(换行符、回车符等)除外。要真正匹配任何字符,请使用 [\s\S]
。例如
> /./.test('\n') false > /[\s\S]/.test('\n') true
\f
(换页符)、\n
(换行符)、\r
(回车符)、\t
(水平制表符)和 \v
(垂直制表符)。\0
匹配 NUL 字符 (\u0000
)。\cA
– \cZ
。\u0000
– \xFFFF
(Unicode 代码单元;请参阅第 24 章)。\x00
– \xFF
。[«charSpecs»]
匹配与 charSpecs
中至少一个字符匹配的任何单个字符。[^«charSpecs»]
匹配与任何 charSpecs
都不匹配的任何单个字符。以下结构都是字符规范
源字符匹配自身。大多数字符都是源字符(即使许多字符在其他地方是特殊的)。只有三个字符不是
\ ] -
像往常一样,您可以通过反斜杠进行转义。如果要匹配破折号而不转义它,它必须是左方括号后的第一个字符或范围的右侧,如下所述。
类转义:允许使用前面列出的任何字符转义和字符类转义。还有一个额外的转义
\b
):在字符类之外,\b
匹配单词边界。不要与 [\b]
混淆,后者匹配退格键。-
),再后跟一个源字符或一个类转义。为了演示如何使用字符类,本例解析了以 ISO 8601 标准格式化的日期
function
parseIsoDate
(
str
)
{
var
match
=
/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/
.
exec
(
str
);
// Other ways of writing the regular expression:
// /^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$/
// /^(\d\d\d\d)-(\d\d)-(\d\d)$/
if
(
!
match
)
{
throw
new
Error
(
'Not an ISO date: '
+
str
);
}
console
.
log
(
'Year: '
+
match
[
1
]);
console
.
log
(
'Month: '
+
match
[
2
]);
console
.
log
(
'Day: '
+
match
[
3
]);
}
以下是交互
> parseIsoDate('2001-12-24') Year: 2001 Month: 12 Day: 24
组的语法如下:
(«pattern»)
是一个捕获组。与 pattern
匹配的任何内容都可以通过反向引用或作为匹配操作的结果进行访问。(?:«pattern»)
是一个非捕获组。仍然将 pattern
与输入进行匹配,但不保存为捕获。因此,该组没有您可以引用的编号(例如,通过反向引用)。\1
、\2
等被称为反向引用;它们指的是先前匹配的组。反斜杠后的数字可以是任何大于或等于 1 的整数,但第一个数字不能是 0。
在本例中,反向引用保证了破折号前后相同数量的 a
> /^(a+)-\1$/.test('a-a') true > /^(a+)-\1$/.test('aaa-aaa') true > /^(a+)-\1$/.test('aa-a') false
本例使用反向引用来匹配 HTML 标记(显然,您通常应该使用适当的解析器来处理 HTML)
> var tagName = /<([^>]+)>[^<]*<\/\1>/; > tagName.exec('<b>bold</b>')[1] 'b' > tagName.exec('<strong>text</strong>')[1] 'strong' > tagName.exec('<strong>text</stron>') null
?
表示匹配零次或一次。*
表示匹配零次或多次。+
表示匹配一次或多次。{n}
表示恰好匹配 n
次。{n,}
表示匹配 n
次或更多次。{n,m}
表示至少匹配 n
次,最多匹配 m
次。默认情况下,量词是贪婪的;也就是说,它们会尽可能多地匹配。您可以通过在任何前面的量词(包括花括号中的范围)后面加上一个问号 (?
) 来获得惰性匹配(尽可能少地匹配)。例如:
> '<a> <strong>'.match(/^<(.*)>/)[1] // greedy 'a> <strong' > '<a> <strong>'.match(/^<(.*?)>/)[1] // reluctant 'a'
因此,.*?
是一个有用的模式,用于匹配所有内容,直到下一个原子出现为止。例如,以下是刚才显示的 HTML 标记正则表达式的更紧凑版本(它使用 [^<]*
而不是 .*?
)
/<(.+?)>.*?<\/\1>/
| 仅在输入的开头匹配。 |
| 仅在输入的结尾匹配。 |
| 仅在单词边界匹配。不要与 |
| 仅在不在单词边界时匹配。 |
| 正向先行断言:仅当 |
| 负向先行断言:仅当 |
本例通过 \b
匹配单词边界
> /\bell\b/.test('hello') false > /\bell\b/.test('ello') false > /\bell\b/.test('ell') true
本例通过 \B
匹配单词内部
> /\Bell\B/.test('ell') false > /\Bell\B/.test('hell') false > /\Bell\B/.test('hello') true
不支持后向断言。手动实现后向断言解释了如何手动实现它。
析取运算符 (|
) 分隔两个备选项;两个备选项中的任何一个都必须匹配,析取才能匹配。备选项是原子(可以选择包含量词)。
该运算符的绑定非常弱,因此您必须小心,不要让备选项扩展得太远。例如,以下正则表达式匹配以 aa
开头或以 bb
结尾的所有字符串
> /^aa|bb$/.test('aaxx') true > /^aa|bb$/.test('xxbb') true
换句话说,析取的绑定比 ^
和 $
还要弱,两个备选项是 ^aa
和 bb$
。如果要匹配两个字符串 'aa'
和 'bb'
,则需要使用括号
/^(aa|bb)$/
同样,如果要匹配字符串 'aab'
和 'abb'
/^a(a|b)b$/
JavaScript 的正则表达式对 Unicode 的支持非常有限。特别是当涉及到星形平面中的代码点时,您必须小心。第 24 章解释了详细信息。
您可以通过字面量或构造函数创建正则表达式,并通过标志配置其工作方式。
创建正则表达式有两种方法:可以使用字面量或构造函数 RegExp
:
字面量 |
| 加载时编译 |
构造函数(第二个参数是可选的) |
| 运行时编译 |
字面量和构造函数在编译时间上有所不同
字面量在加载时编译。以下代码在求值时将导致异常
function
foo
()
{
/[/;
}
构造函数在调用时编译正则表达式。以下代码不会导致异常,但调用 foo()
会:
function
foo
()
{
new
RegExp
(
'['
);
}
因此,您通常应该使用字面量,但如果要动态组装正则表达式,则需要使用构造函数。
标志是正则表达式字面量的后缀和正则表达式构造函数的参数;它们修改正则表达式的匹配行为。存在以下标志:
简称 | 全称 | 说明 |
|
| 给定的正则表达式被多次匹配。影响多种方法,尤其是 |
|
| 在尝试匹配给定的正则表达式时忽略大小写。 |
|
| 在多行模式下,开始运算符 |
简称用于字面量前缀和构造函数参数(请参阅下一节中的示例)。全称用于正则表达式的属性,这些属性指示在创建过程中设置了哪些标志。
正则表达式具有以下实例属性:
标志:指示设置了哪些标志的布尔值
global
:是否设置了标志 /g
?ignoreCase
:是否设置了标志 /i
?multiline
:是否设置了标志 /m
?多次匹配的数据(设置了标志 /g
)
lastIndex
是下次继续搜索的索引。以下是访问标志实例属性的示例
> var regex = /abc/i; > regex.ignoreCase true > regex.multiline false
在本例中,我们首先使用字面量创建相同的正则表达式,然后使用构造函数创建,并使用 test()
方法确定它是否与字符串匹配:
> /abc/.test('ABC') false > new RegExp('abc').test('ABC') false
在本例中,我们创建一个忽略大小写的正则表达式(标志 /i
)
> /abc/i.test('ABC') true > new RegExp('abc', 'i').test('ABC') true
test()
方法检查正则表达式 regex
是否与字符串 str
匹配:
regex
.
test
(
str
)
test()
的操作方式取决于是否设置了标志 /g
。
如果未设置标志 /g
,则该方法检查 str
中的某个位置是否存在匹配项。例如
> var str = '_x_x'; > /x/.test(str) true > /a/.test(str) false
如果设置了标志 /g
,则该方法会针对 str
中 regex
的每个匹配项返回 true
。属性 regex.lastIndex
包含最后一个匹配项之后的索引
> var regex = /x/g; > regex.lastIndex 0 > regex.test(str) true > regex.lastIndex 2 > regex.test(str) true > regex.lastIndex 4 > regex.test(str) false
search()
方法 在 str
中查找与 regex
的匹配项:
str
.
search
(
regex
)
如果存在匹配项,则返回找到匹配项的索引。否则,结果为 -1
。执行搜索时会忽略 regex
的属性 global
和 lastIndex
(并且不会更改 lastIndex
)。
例如
> 'abba'.search(/b/) 1 > 'abba'.search(/x/) -1
如果 search()
的参数不是正则表达式,则会将其转换为正则表达式
> 'aaab'.search('^a+b+$') 0
以下方法调用在将 regex
与 str
进行匹配时捕获组:
var
matchData
=
regex
.
exec
(
str
);
如果没有匹配项,则 matchData
为 null
。否则,matchData
是一个 匹配结果,它是一个具有两个附加属性的数组
input
是完整的输入字符串。index
是找到匹配项的索引。如果未设置标志 /g
,则仅返回第一个匹配项
> var regex = /a(b+)/; > regex.exec('_abbb_ab_') [ 'abbb', 'bbb', index: 1, input: '_abbb_ab_' ] > regex.lastIndex 0
如果设置了标志 /g
,则重复调用 exec()
会返回所有匹配项。返回值 null
表示没有更多匹配项。属性 lastIndex
指示下次匹配将从哪里继续
> var regex = /a(b+)/g; > var str = '_abbb_ab_'; > regex.exec(str) [ 'abbb', 'bbb', index: 1, input: '_abbb_ab_' ] > regex.lastIndex 6 > regex.exec(str) [ 'ab', 'b', index: 7, input: '_abbb_ab_' ] > regex.lastIndex 10 > regex.exec(str) null
这里我们循环遍历匹配项
var
regex
=
/a(b+)/g
;
var
str
=
'_abbb_ab_'
;
var
match
;
while
(
match
=
regex
.
exec
(
str
))
{
console
.
log
(
match
[
1
]);
}
我们得到以下输出
bbb b
以下方法调用 将 regex
与 str
进行匹配:
var
matchData
=
str
.
match
(
regex
);
如果未设置 regex
的标志 /g
,则此方法的工作方式类似于 RegExp.prototype.exec()
> 'abba'.match(/a/) [ 'a', index: 0, input: 'abba' ]
如果设置了该标志,则该方法返回一个数组,其中包含 str
中所有匹配的子字符串(即,每个匹配项的组 0),如果没有匹配项,则返回 null
> 'abba'.match(/a/g) [ 'a', 'a' ] > 'abba'.match(/x/g) null
replace()
方法在 字符串 str
中搜索与 search
的匹配项,并将其替换为 replacement
:
str
.
replace
(
search
,
replacement
)
可以通过多种方式指定这两个参数
搜索
字符串或正则表达式
/g
标志的正则表达式。这是出乎意料的,也是一个主要的陷阱。global
标志,否则只会尝试匹配一次正则表达式。替换
字符串或函数
如果 replacement
是字符串,则其内容将按字面用于替换匹配项。唯一的例外是特殊字符美元符号 ($
),它启动了所谓的 替换指令:
$n
插入匹配项中的组 n。n
必须至少为 1($0
没有特殊含义)。匹配的子字符串
$`
(反引号)插入匹配项之前的文本。$&
插入完整的匹配项。$'
(撇号)插入匹配项之后的文本。$$
插入单个 $
。此示例引用匹配的子字符串及其前缀和后缀
> 'axb cxd'.replace(/x/g, "[$`,$&,$']") 'a[a,x,b cxd]b c[axb c,x,d]d'
此示例引用一个组
> '"foo" and "bar"'.replace(/"(.*?)"/g, '#$1#') '#foo# and #bar#'
如果 replacement
是函数,则它计算要替换匹配项的字符串。此函数具有 以下签名:
function
(
completeMatch
,
group_1
,
...,
group_n
,
offset
,
inputStr
)
completeMatch
与之前的 $&
相同,offset
指示找到匹配项的位置,inputStr
是要匹配的内容。因此,您可以使用特殊变量 arguments
来访问组(组 1 通过 arguments[1]
访问,依此类推)。例如
> function replaceFunc(match) { return 2 * match } > '3 apples and 5 oranges'.replace(/[0-9]+/g, replaceFunc) '6 apples and 10 oranges'
如果必须多次调用在 设置了 /g
标志的正则表达式上调用的方法才能返回所有结果,则会出现问题。对于以下两种方法,情况就是这样:
RegExp.prototype.test()
RegExp.prototype.exec()
然后,JavaScript 将正则表达式滥用作迭代器,作为结果序列中的指针。这会导致问题
/g
正则表达式不能内联例如
// Don’t do that:
var
count
=
0
;
while
(
/a/g
.
test
(
'babaa'
))
count
++
;
前面的循环是无限的,因为为每个循环迭代创建了一个新的正则表达式,这将重新启动对结果的迭代。因此,必须重写代码
var
count
=
0
;
var
regex
=
/a/g
;
while
(
regex
.
test
(
'babaa'
))
count
++
;
以下是另一个示例
// Don’t do that:
function
extractQuoted
(
str
)
{
var
match
;
var
result
=
[];
while
((
match
=
/"(.*?)"/g
.
exec
(
str
))
!=
null
)
{
result
.
push
(
match
[
1
]);
}
return
result
;
}
调用前面的函数将再次导致无限循环。正确的版本是(稍后将解释为什么将 lastIndex
设置为 0)
var
QUOTE_REGEX
=
/"(.*?)"/g
;
function
extractQuoted
(
str
)
{
QUOTE_REGEX
.
lastIndex
=
0
;
var
match
;
var
result
=
[];
while
((
match
=
QUOTE_REGEX
.
exec
(
str
))
!=
null
)
{
result
.
push
(
match
[
1
]);
}
return
result
;
}
使用函数
> extractQuoted('"hello", "world"') [ 'hello', 'world' ]
最佳做法是不进行内联(这样您就可以为正则表达式指定描述性名称)。但是您必须意识到您不能这样做,即使是在快速破解中也不行。
/g
正则表达式作为参数test()
和 exec()
的代码必须小心处理作为参数传递给它的正则表达式。它的标志 /g
必须处于活动状态,并且为了安全起见,它的 lastIndex
应该设置为零(下一个示例中提供了解释)。/g
正则表达式(例如,常量)lastIndex
属性设置为零(下一个示例中提供了解释)。由于迭代取决于 lastIndex
,因此此类正则表达式不能同时在多个迭代中使用。以下示例说明了问题 2。这是一个函数的简单实现,该函数计算字符串 str
中正则表达式 regex
的匹配项数量
// Naive implementation
function
countOccurrences
(
regex
,
str
)
{
var
count
=
0
;
while
(
regex
.
test
(
str
))
count
++
;
return
count
;
}
以下是使用此函数的示例
> countOccurrences(/x/g, '_x_x') 2
第一个问题是,如果未设置正则表达式的 /g
标志,则此函数将进入无限循环。例如
countOccurrences
(
/x/
,
'_x_x'
)
// never terminates
第二个问题是,如果 regex.lastIndex
不是 0,则该函数无法正常工作,因为该属性指示从哪里开始搜索。例如
> var regex = /x/g; > regex.lastIndex = 2; > countOccurrences(regex, '_x_x') 1
以下实现解决了这两个问题
function
countOccurrences
(
regex
,
str
)
{
if
(
!
regex
.
global
)
{
throw
new
Error
(
'Please set flag /g of regex'
);
}
var
origLastIndex
=
regex
.
lastIndex
;
// store
regex
.
lastIndex
=
0
;
var
count
=
0
;
while
(
regex
.
test
(
str
))
count
++
;
regex
.
lastIndex
=
origLastIndex
;
// restore
return
count
;
}
更简单的替代方法是使用 match()
function
countOccurrences
(
regex
,
str
)
{
if
(
!
regex
.
global
)
{
throw
new
Error
(
'Please set flag /g of regex'
);
}
return
(
str
.
match
(
regex
)
||
[]).
length
;
}
有一个可能的陷阱:如果设置了 /g
标志并且没有匹配项,则 str.match()
返回 null
。我们在前面的代码中通过在 match()
的结果不为真时使用 []
来避免此陷阱。
本节提供了一些在 JavaScript 中使用正则表达式的提示和技巧。
有时,当您 手动组合正则表达式时,您希望按字面使用给定的字符串。 这意味着所有特殊字符(例如,*
、[
)都不应被解释为特殊字符,所有字符都需要转义。JavaScript 没有用于这种引用的内置方法,但您可以编写自己的函数 quoteText
,该函数的工作方式如下::
> console.log(quoteText('*All* (most?) aspects.')) \*All\* \(most\?\) aspects\.
如果您需要对多个匹配项进行搜索和替换,则此类函数特别方便。然后,要搜索的值必须是设置了 global
标志的正则表达式。使用 quoteText()
,您可以使用任意字符串。该函数如下所示
function
quoteText
(
text
)
{
return
text
.
replace
(
/[\\^$.*+?()[\]{}|=!<>:-]/g
,
'\\$&'
);
}
所有特殊字符都会被转义,因为您可能希望在括号或方括号内引用多个字符。
如果您不使用 断言(例如 ^
和 $
),则大多数正则表达式方法都会在任何地方找到模式。例如:
> /aa/.test('xaay') true > /^aa$/.test('xaay') false
这是一种罕见的用例,但有时您需要一个匹配所有内容或不匹配任何内容的正则表达式。例如,一个函数可能有一个带有用于过滤的正则表达式的参数。如果缺少该参数,则为其指定一个默认值,即匹配所有内容的正则表达式。
空正则表达式匹配所有内容。我们可以像这样创建一个基于该正则表达式的 RegExp
实例
> new RegExp('').test('dfadsfdsa') true > new RegExp('').test('') true
但是,空正则表达式字面量将是 //
,JavaScript 将其解释为注释。因此,以下是最接近通过字面量实现的目标:/(?:)/
(空非捕获组)。该组匹配所有内容,但不捕获任何内容,这会影响 exec()
返回的结果。甚至 JavaScript 本身在显示空正则表达式时也使用前面的表示形式
> new RegExp('') /(?:)/
空正则表达式有一个反义词,即不匹配任何内容的正则表达式
> var never = /.^/; > never.test('abc') false > never.test('') false
后向断言是一种 断言。 与前向断言类似,模式用于检查输入中当前位置的某些内容,但在其他情况下会被忽略。与前向断言相反,模式的匹配必须在当前位置 结束(而不是从当前位置开始)。
以下函数将字符串 'NAME'
的每次出现替换为参数 name
的值,但前提是该出现前面没有引号。我们通过“手动”检查当前匹配项之前的字符来处理引号
function
insertName
(
str
,
name
)
{
return
str
.
replace
(
/NAME/g
,
function
(
completeMatch
,
offset
)
{
if
(
offset
===
0
||
(
offset
>
0
&&
str
[
offset
-
1
]
!==
'"'
))
{
return
name
;
}
else
{
return
completeMatch
;
}
}
);
}
> insertName('NAME "NAME"', 'Jane') 'Jane "NAME"' > insertName('"NAME" NAME', 'Jane') '"NAME" Jane'
另一种方法是在正则表达式中包含可能转义的字符。然后,您必须临时将前缀添加到要搜索的字符串中;否则,您将错过该字符串开头的匹配项
function
insertName
(
str
,
name
)
{
var
tmpPrefix
=
' '
;
str
=
tmpPrefix
+
str
;
str
=
str
.
replace
(
/([^"])NAME/g
,
function
(
completeMatch
,
prefix
)
{
return
prefix
+
name
;
}
);
return
str
.
slice
(
tmpPrefix
.
length
);
// remove tmpPrefix
}
原子(请参阅 原子:常规)
.
(点)匹配除行终止符(例如,换行符)之外的所有内容。使用 [\s\S]
来真正匹配所有内容。 字符类转义
\d
匹配数字 ([0-9]
);\D
匹配非数字 ([^0-9]
)。\w
匹配拉丁字母数字字符和下划线 ([A-Za-z0-9_]
);\W
匹配所有其他字符。\s
匹配所有空白字符(空格、制表符、换行符等);\S
匹配所有非空白字符。字符类(字符集):[...]
和 [^...]
[abc]
(除 \ ] -
之外的所有字符都匹配自身)[\d\w]
[A-Za-z0-9]
组
(...)
;反向引用:\1
(?:...)
贪婪
? * +
{n} {n,} {n,m}
?
。^ $
\b \B
(?=...)
(模式必须在后面出现,但会被忽略)(?!...)
(模式不能在后面出现,但会被忽略)析取:|
创建正则表达式(参见创建正则表达式)
/xyz/i
(加载时编译)new RegExp('xzy', 'i')
(运行时编译)标志(参见标志)
/g
(影响多个正则表达式方法)/i
/m
(^
和 $
匹配每行,而不是整个输入)方法
regex.test(str)
:是否存在匹配(参见RegExp.prototype.test:是否存在匹配?)?
/g
:是否存在匹配?/g
:只要有匹配就返回 true
。str.search(regex)
:匹配项的索引位置(参见String.prototype.search:匹配项的索引位置?)?
regex.exec(str)
:捕获组(参见RegExp.prototype.exec:捕获组)?
/g
:仅捕获第一个匹配项的组(调用一次)/g
:捕获所有匹配项的组(重复调用;如果没有更多匹配项,则返回 null
)
str.match(regex)
:捕获组或返回所有匹配的子字符串(参见String.prototype.match:捕获组或返回所有匹配的子字符串)
/g
:捕获组/g
:以数组形式返回所有匹配的子字符串
str.replace(search, replacement)
:搜索和替换(参见String.prototype.replace:搜索和替换)
search
:字符串或正则表达式(使用后者,设置 /g
!)replacement
:字符串(带有 $1
等)或返回字符串的函数(arguments[1]
是组 1,等等)有关使用标志 /g
的提示,请参见标志 /g 的问题。
Mathias Bynens (@mathias) 和 Juan Ignacio Dopazo (@juandopazo) 建议使用 match()
和 test()
来统计出现次数,Šime Vidas (@simevidas) 提醒我如果没有任何匹配项,使用 match()
时要小心。全局标志导致无限循环的陷阱来自 Andrea Giammarchi 的演讲 (@webreflection)。Claude Pache 告诉我应该在 quoteText()
中转义更多字符。