原文链接:http://javascript.info/regexp-groups,translate with ❤️ by zhangbao.

模式的一部分可以用圆括号 (...) 包围,这称为”捕获分组”。

会有两个结果:

  1. 当使用 String#match 或者 RegExp#exec 方法的时候,它允许将完整匹配里的分组捕获匹配项,放入最终返回的匹配数组里。

  2. 如果我们在圆括号后面使用了量词,圆括号里的内容就会被看成一个整体,而不是最后一个字符了。

例子

下面例子模式里的 (go)+ 是查找一个或多一个的 ‘go‘:

  1. alert( 'Gogogo now!'.match(/(go)+/i) ); // Gogogo,go

如果没有圆括号的话,模式 /go+/ 表示的是 g 后面跟一个或者重复多个的字母 o。例如,goooo 或者 gooooooooo

圆括号里将两个字母 (go) 组合在一起了。

让我们再举一个更复杂点的例子——匹配 Email 的正则表达式。

Email 的例子:

  1. my@mail.com
  2. john.smith@site.com.uk

模式如:[-.\w]+@([\w-]+\.)+[\w-]{2,20}

  • 第一部分是 @ 符号前面的部分:单词、点和连字符,即 [-.\w],像 John.smith

  • 然后是 @

  • 然后是域名。可能是一个二级域名 site.com 或者带子域名的 host.site.com.uk。我们可以用”一个单词后面跟一个点”重复匹配若干次来表示,用于满足存在子域名的情况:mail. 或者 site.com.,最后一部分就是”单词” .com 或者 .uk 这样的单词了。

单词后面跟一个点用 (\w+\.)+(重复)。最后一个单词不包含点,所以只用了 \w{2,20},量词 {2,20} 限制 的是长度,因为域名块像 .uk.com 或者 .meseum 这些单词的长度是不能超过 20 个字符的。

所以匹配就变成了 (\w+\.)+\w{2,20},现在将 \w 替换成 [\w-],因为连字符在域名里也是被允许的,所以就得到了最终看到的结果。

其实这个正则表达式并不完美,但用是可以用的。足够好来修正错误或偶尔的失误。

例如,下面就是用了这个正则表达式查找字符串里的所有 Email 地址:

  1. let reg = /[-.\w]+@([\w-]+\.)+[\w-]{2,20}/g;
  2. alert("my@mail.com @ his@site.com.uk".match(reg)); // my@mail.com,his@site.com.uk

圆括号里的内容

圆括号是从左到右编号的,搜索引擎记忆住每个圆括号里的匹配项内容,用于在模式或者替换字符串里引用。

例如,我们可以使用模式 <.+?> 模式匹配 HTML 标签,通常我们会想要在结果之后做一些事情。

如果我们将 <...> 里的内容用圆括号括起来,我们可以得到这样的结果·:

  1. let str = '<h1>你好,世界!</h1>'
  2. let reg = /<(.+?)>/;
  3. alert( str.match(reg) ); // <h1>,h1

String#macth 方法只有在正则表达式没有 g 标记的情况下,才会返回分组匹配信息。

如果我们需要知道所有的分组匹配信息的话,就要用到 正则和字符串方法 一章里介绍的 RegExp.exec 方法:

  1. let str = '<h1>Hello, world!</h1>';
  2. // 匹配两种情况: 开始标签 <h1> 和关闭标签 </h1>。
  3. let reg = /<(.+?)>/g;
  4. let match;
  5. while (match = reg.exec(str)) {
  6. // 第一个匹配项:<h1>,h1
  7. // 第二个匹配项:</h1>,/h1
  8. alert(match);
  9. }

现在我们使用 <(.+?)> 找到了两个人匹配项,每个分组匹配结果都与完整匹配结果放在一个数组里了。

内嵌分组

圆括号可以内嵌,这种情况下编号还是从左到右的。

例如,我们搜索标签 <span class="my">,需要得到的结果里包含:

  1. 标签里的整个内容:span class="my"

  2. 标签名:span

  3. 标签特性:class="my"

下面我们来写表达式:

  1. let str = 'span class="my"';
  2. let reg = /<(([a-z]+)\s*([^>]*)>/;
  3. let result = str.match(reg);
  4. alert( result ); // <span class="my">, span class="my", span, class="my"

分组情况是这样的:

分组捕获 - 图1

结果 result 里的第一个元素总是完整匹配的那个项目。

分组,是从做到右编号的,第一个分组对应 result[1],包含的是整个标签内容。

result[2] 对应第二个分组结果,也就是第二个开启 ( 到最近关闭 ) 的位置,包含的是标签名。我们不需要捕获空格,所以标签特性内容分在了 result[3] 里。

如果一个分组是可选的,并且不存在匹配内容,那么对应 result 的索引仍然存在(值为 undefined)。

例如,考虑正则 a(z)?(c)?,它查找 a 后面可选的 zc

如果我们用 a 去 match 这个正则的话,会得到这样的结果:

  1. let match = 'a'.match(/a(z)?(c)?/);
  2. alert( match.length ); // 3
  3. alert( match[0] ); // a (整个匹配项)
  4. alert( match[1] ); // undefined
  5. alert( match[2] ); // undefined

结果数组的长度为 3,但是所有的分组匹配内容都是没有的。

这里还有一个针对 ack 这个字符串的更加复杂的匹配:

  1. let match = 'ack'.match(/a(z)?(c)?/)
  2. alert( match.length ); // 3
  3. alert( match[0] ); // ac (整个匹配项)
  4. alert( match[1] ); // undefined, 因为 (z)? 没有匹配的内容
  5. alert( match[2] ); // c

数组的长度还是 3,但是由于 (z)? 没有任何匹配内容,所有最终的结果是 ["ac", undefined, "c"]

不要捕获分组用 ?:

有时我们需要括号来正确地应用量词,但是我们不希望它们的内容出现在结果数组中。

可以在每个分组的开头添加 ?:表示在结果数组中排除此分组统计。

例如,我们要查找 (go)+,但又不希望将内容 (go) 记住在结果里,就可以写成 (?:go)+

在下面的例子中,我们只获得”John“这个名称作为第二个分组在结果数组的一个成员:

  1. let str = 'Gogo John';
  2. // 将 Gogo 排除在分组之外
  3. let reg = /(?:go)+ (\w+)/i;
  4. let result = str.match(reg);
  5. alert( result.length );
  6. alert( result[1] ); // John

(完)