模式的一部分可以放在括号 (...)
中。这被称为“捕获组”。
这有两个效果
- 它允许将匹配的一部分作为结果数组中的单独项获取。
- 如果我们在括号后放置一个量词,它将应用于整个括号。
示例
让我们看看括号在示例中的工作方式。
示例:gogogo
没有括号,模式 go+
表示 g
字符,后跟 o
重复一次或多次。例如,goooo
或 gooooooooo
。
括号将字符分组在一起,因此 (go)+
表示 go
、gogo
、gogogo
等等。
alert( 'Gogogo now!'.match(/(go)+/ig) ); // "Gogogo"
示例:域名
让我们做一些更复杂的事情 - 一个用于搜索网站域名的正则表达式。
例如
mail.com
users.mail.com
smith.users.mail.com
正如我们所见,域名由重复的单词组成,每个单词后都有一个点,最后一个单词除外。
在正则表达式中,它是 (\w+\.)+\w+
let regexp = /(\w+\.)+\w+/g;
alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com
搜索有效,但模式无法匹配包含连字符的域名,例如 my-site.com
,因为连字符不属于类 \w
。
我们可以通过将 \w
替换为 [\w-]
来解决此问题,除了最后一个单词之外,所有单词都替换:([\w-]+\.)+\w+
。
示例:电子邮件
前面的示例可以扩展。我们可以根据它创建电子邮件的正则表达式。
电子邮件格式为:name@domain
。任何单词都可以作为名称,允许使用连字符和点。在正则表达式中,它是 [-.\w]+
。
模式
let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g;
alert("[email protected] @ [email protected]".match(regexp)); // [email protected], [email protected]
该正则表达式并不完美,但大部分情况下有效,有助于修复意外的错误输入。唯一真正可靠的电子邮件检查方法只能通过发送邮件来完成。
匹配中的括号内容
括号从左到右编号。搜索引擎会记住每个括号匹配的内容,并允许在结果中获取它。
方法 str.match(regexp)
,如果 regexp
没有标志 g
,则查找第一个匹配项并将其作为数组返回
- 在索引
0
处:完全匹配。 - 在索引
1
处:第一个括号的内容。 - 在索引
2
处:第二个括号的内容。 - …等等…
例如,我们想找到 HTML 标签 <.*?>
,并对其进行处理。将标签内容(角度之间的内容)放在单独的变量中会很方便。
让我们将内部内容用括号括起来,如下所示:<(.*?)>
。
现在,我们将在结果数组中获得整个标签 <h1>
及其内容 h1
let str = '<h1>Hello, world!</h1>';
let tag = str.match(/<(.*?)>/);
alert( tag[0] ); // <h1>
alert( tag[1] ); // h1
嵌套组
括号可以嵌套。在这种情况下,编号也是从左到右进行的。
例如,在 <span class="my">
中搜索标签时,我们可能对以下内容感兴趣
- 整个标签内容:
span class="my"
。 - 标签名称:
span
。 - 标签属性:
class="my"
。
让我们为它们添加括号:<(([a-z]+)\s*([^>]*))>
。
以下是它们的编号方式(从左到右,按开括号)
实际应用
let str = '<span class="my">';
let regexp = /<(([a-z]+)\s*([^>]*))>/;
let result = str.match(regexp);
alert(result[0]); // <span class="my">
alert(result[1]); // span class="my"
alert(result[2]); // span
alert(result[3]); // class="my"
result
的零索引始终包含完全匹配。
然后是组,按开括号从左到右编号。第一个组作为 result[1]
返回。这里它包含整个标签内容。
然后在result[2]
中存放的是第二个开括号
中的组 - 标签名,然后在([a-z]+)
result[3]
中存放的是标签:
。([^>]*)
字符串中每个组的内容
可选组
即使一个组是可选的,并且在匹配中不存在(例如,具有量词
),相应的(...)?
result
数组项仍然存在,并且等于undefined
。
例如,让我们考虑正则表达式
。它查找a(z)?(c)?
"a"
,后面可选地跟着"z"
,后面可选地跟着"c"
。
如果我们在只有一个字母
的字符串上运行它,那么结果是a
let match = 'a'.match(/a(z)?(c)?/);
alert( match.length ); // 3
alert( match[0] ); // a (whole match)
alert( match[1] ); // undefined
alert( match[2] ); // undefined
该数组的长度为3
,但所有组都是空的。
下面是字符串
的更复杂的匹配ac
let match = 'ac'.match(/a(z)?(c)?/)
alert( match.length ); // 3
alert( match[0] ); // ac (whole match)
alert( match[1] ); // undefined, because there's nothing for (z)?
alert( match[2] ); // c
数组长度是固定的:3
。但是组
没有内容,所以结果是(z)?
["ac", undefined, "c"]
。
使用组搜索所有匹配项:matchAll
matchAll
是一个新方法,可能需要 polyfillmatchAll
方法在旧浏览器中不受支持。
可能需要 polyfill,例如 https://github.com/ljharb/String.prototype.matchAll。
当我们搜索所有匹配项(标志
)时,g
match
方法不会返回组的内容。
例如,让我们在一个字符串中查找所有标签
let str = '<h1> <h2>';
let tags = str.match(/<(.*?)>/g);
alert( tags ); // <h1>,<h2>
结果是一个匹配项数组,但没有关于每个匹配项的详细信息。但在实践中,我们通常需要结果中捕获组的内容。
要获取它们,我们应该使用str.matchAll(regexp)
方法进行搜索。
它是在match
之后很久才添加到 JavaScript 语言中的,是它的“新改进版本”。
与match
一样,它查找匹配项,但有 3 个区别
- 它返回的不是数组,而是一个可迭代对象。
- 当存在标志
时,它将每个匹配项作为包含组的数组返回。g
- 如果没有匹配项,它返回的不是
null
,而是一个空的可迭代对象。
例如
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
// results - is not an array, but an iterable object
alert(results); // [object RegExp String Iterator]
alert(results[0]); // undefined (*)
results = Array.from(results); // let's turn it into array
alert(results[0]); // <h1>,h1 (1st tag)
alert(results[1]); // <h2>,h2 (2nd tag)
正如我们所见,第一个区别非常重要,如(*)
行所示。我们无法将匹配项作为results[0]
获取,因为该对象是一个伪数组。我们可以使用Array.from
将其转换为真正的Array
。有关伪数组和可迭代对象的更多详细信息,请参阅文章 可迭代对象。
如果我们循环遍历结果,则不需要Array.from
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
for(let result of results) {
alert(result);
// first alert: <h1>,h1
// second: <h2>,h2
}
…或者使用解构
let [tag1, tag2] = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
由matchAll
返回的每个匹配项都具有与match
(不带标志
)返回的相同格式:它是一个数组,具有附加属性g
index
(字符串中的匹配项索引)和input
(源字符串)
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
let [tag1, tag2] = results;
alert( tag1[0] ); // <h1>
alert( tag1[1] ); // h1
alert( tag1.index ); // 0
alert( tag1.input ); // <h1> <h2>
matchAll
的结果是可迭代对象而不是数组?为什么该方法的设计如此?原因很简单——为了优化。
调用matchAll
不会执行搜索。相反,它返回一个可迭代对象,最初没有结果。每次我们迭代它时,例如在循环中,都会执行搜索。
因此,将找到所需的结果,而不是更多。
例如,文本中可能存在 100 个匹配项,但在 for..of
循环中我们只找到了 5 个,然后决定足够了并执行了 break
。然后引擎就不会浪费时间寻找其他 95 个匹配项。
命名组
记住组的编号很困难。对于简单的模式来说,这是可行的,但对于更复杂的模式来说,计算括号很不方便。我们有一个更好的选择:给括号命名。
这可以通过在开括号后立即添加 ?<name>
来实现。
例如,让我们查找格式为“年-月-日”的日期
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
let str = "2019-04-30";
let groups = str.match(dateRegexp).groups;
alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30
如您所见,组位于匹配的 .groups
属性中。
要查找所有日期,我们可以添加标志 g
。
我们还需要 matchAll
来获取完整的匹配项,以及组
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30 2020-01-01";
let results = str.matchAll(dateRegexp);
for(let result of results) {
let {year, month, day} = result.groups;
alert(`${day}.${month}.${year}`);
// first alert: 30.10.2019
// second: 01.01.2020
}
在替换中捕获组
方法 str.replace(regexp, replacement)
用 regexp
在 str
中替换所有匹配项,允许在 replacement
字符串中使用括号内容。这可以通过 $n
来实现,其中 n
是组号。
例如,
let str = "John Bull";
let regexp = /(\w+) (\w+)/;
alert( str.replace(regexp, '$2, $1') ); // Bull, John
对于命名括号,引用将是 $<name>
。
例如,让我们将日期从“年-月-日”格式化为“日.月.年”
let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30, 2020-01-01";
alert( str.replace(regexp, '$<day>.$<month>.$<year>') );
// 30.10.2019, 01.01.2020
使用 ? 的非捕获组
有时我们需要括号来正确应用量词,但我们不希望它们的内容出现在结果中。
可以通过在开头添加 ?:
来排除一个组。
例如,如果我们想找到 (go)+
,但不想将括号内容 (go
) 作为单独的数组项,我们可以写:(?:go)+
。
在下面的示例中,我们只获得了名称 John
作为匹配项的单独成员
let str = "Gogogo John!";
// ?: excludes 'go' from capturing
let regexp = /(?:go)+ (\w+)/i;
let result = str.match(regexp);
alert( result[0] ); // Gogogo John (full match)
alert( result[1] ); // John
alert( result.length ); // 2 (no more items in the array)
总结
括号将正则表达式的部分内容分组在一起,以便量词可以将其作为一个整体应用。
括号组从左到右编号,并且可以选择使用 (?<name>...)
命名。
组匹配的内容可以在结果中获得
- 方法
str.match
仅在没有标志g
的情况下返回捕获组。 - 方法
str.matchAll
始终返回捕获组。
如果括号没有名称,则它们的内容可以通过其编号在匹配数组中获得。命名括号也可以在 groups
属性中获得。
我们也可以在替换字符串中使用括号中的内容,通过数字$n
或名称$<name>
。
可以通过在组的开头添加?:
来排除组的编号。当我们需要对整个组应用量词,但不想将其作为结果数组中的单独项时,就会使用它。我们也不能在替换字符串中引用这样的括号。
评论
<code>
标签,对于多行代码,请将它们包装在<pre>
标签中,对于超过 10 行的代码,请使用沙箱 (plnkr,jsbin,codepen…)