正则模式的一部分可以用括号括起来 (...),由此构成一个『捕获组』。

这有两个作用:

  1. 当使用 String#matchRegExp#exec 方法时,它允许你把匹配到的部分放到一个独立的数组项里面。
  2. 如果我们在括号之后加上量词,那么它会应用到这个整体,而非最后一个字符。

例子

以下例子中的模式 (go)+ 将会查找一个或多个 'go'

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

如果没有括号,模式 /go+/ 则表示 g 之后跟上一个或多个 o。比如:goooo 或者 gooooooooo

捕获括号将 (go) 划为了一组。

让我们尝试一个更复杂的例子 —— 一个匹配 email 地址的正则表达式。

例如:

my@mail.com
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.museum 等等,但是其长度不能超过 20 个字符。

    因此域名部分的匹配模式为 (\w+\.)+\w{2,20}。现在我们可以用 [\w-] 替换 \w,因为域名也可以包含中划线-。由此我们得到了最终的结果。

这条正则并不完美,但是通常来说它是有效的。它很简短,并且足以让你修正错误,以及时常出现的拼写问题。

举个例子,这里我们可以找到字符串中所有的 email 地址:

let reg = /[-.\w]+@([\w-]+\.)+[\w-]{2,20}/g;

alert("my@mail.com @ his@site.com.uk".match(reg)); // my@mail.com,his@site.com.uk

捕获内容

捕获括号会按从左往右的顺序标上序号。查找引擎会记住每个括号内的内容,并且允许你在模式以及替换字符串中引用它。

举例来说,我们可以使用一个(简化版)的模式 <.*?> 来查找一个 HTML 标签。一般来说,我们会希望对这个结果做些什么。

如果我们把 <...> 里面的内容放到一对捕获括号里,那么我们通过这种方法来引用它:

let str = '<h1>Hello, world!</h1>';
let reg = /<(.*?)>/;

alert( str.match(reg) ); // Array: ["<h1>", "h1"]

String#match 只有在正则表达式中没有 /.../g 标记时才会返回组。

如果我们需要查找所有的匹配组,那么我们可以使用在 RegExp 和 String 的方法 中介绍过的 RegExp#exec 方法:

let str = '<h1>Hello, world!</h1>';

// 两组匹配:起始标签 <h1>和闭合标签</h1>
let reg = /<(.*?)>/g;

let match;

while (match = reg.exec(str)) {
  // 第一次显示匹配:<h1>,h1
  // 之后显示匹配:</h1>,/h1
  alert(match);
}

如此我们便得到了 <(.*?)> 的两个匹配项,他们中的每一个都包括完整的匹配和对应的捕获组。

嵌套捕获组

捕获括号是可以嵌套的。在这种情况下,依然是从左往右编号。

举个例子,当对标签 <span class="my"> 进行查找时,我们可能感兴趣的有:

  1. 标签整体的内容:span class="my"
  2. 标签名:span
  3. 标签的属性:class="my"

让我们为它们加上捕获括号:

let str = '<span class="my">';

let reg = /<(([a-z]+)\s*([^>]*))>/;

let result = str.match(reg);
alert(result); // <span class="my">, span class="my", span, class="my"

它看起来像这样:

result 的首项永远是完整的匹配结果。

之后就是各个捕获组,从左往右依次排开。第一个左括号将会匹配到第一个捕获组 result[1]。在这个例子中,它涵盖了整个标签的内容。

result[2] 对应第二个左括号 ( 到与其对应的右括号 ) 之间的内容 —— 标签名。再然后,我们跳过空格,将所有属性划为一组,对应 result[3]

如果某个捕获组是可选的,且在匹配中没有找到对应项,那么在相应的匹配结果中,该项依然会存在(值为 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

该数组包含三项,但是所有的捕获组都为空。

对于 ack,情况要更复杂一些:

let match = 'ack'.match(/a(z)?(c)?/)

alert( match.length ); // 3
alert( match[0] ); // ac (whole match)
alert( match[1] ); // undefined,因为 (z)? 没有匹配项
alert( match[2] ); // c

数组的长度依然是 3。但是因为捕获组 (z)? 没有对应项,所以结果为 ["ac", undefined, "c"]

非捕获组 ?:

某些时候,我们会希望使用括号来正确设置量词,但是并不希望其内容出现在结果数组中。

你可以通过在开头加上 ?: 从而在结果中排除该组。

例如,我们希望找到 (go)+,但是并不希望其内容(go)出现在单独的数组项中,那么我们可以这样写:(?:go)+

下面的例子中,只有名字『John』会作为一个独立项出现在 results 数组里:

let str = "Gogo John!";
// 避免捕获 Gogo
let reg = /(?:go)+ (\w+)/i;

let result = str.match(reg);

alert( result.length ); // 2
alert( result[1] ); // John

任务

编写一个正则来匹配 #abc#abcdef 格式的颜色。即:# 后接三位或六位 16 进制数。

使用案例:

let reg = /your regexp/g;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(reg) ); // #3f3 #AA0ef

注:必须为三位或六位,#abcd 这种不应该被匹配。

查找三位颜色 #abc 的正则表达式为:/#[a-f0-9]{3}/i

我们可以添加额外三位 16 进制数,不多也不少。这三位可能有,也可能没有。

最简单的方式 —— 直接附加上去:/#[a-f0-9]{3}([a-f0-9]{3})?/i

但是,还有一种更讨巧的方法:/#([a-f0-9]{3}){1,2}/i

这里我们把正则 [a-f0-9]{3} 放置在括号内,并且应用量词 {1,2}

实际操作:

let reg = /#([a-f0-9]{3}){1,2}/gi;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(reg) ); // #3f3 #AA0ef #abc

不过这里有个小问题:这个模式会在 #abcd 中找到 #abc。为了避免这种情况,我们可以在最后加上 \b

let reg = /#([a-f0-9]{3}){1,2}\b/gi;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(reg) ); // #3f3 #AA0ef

编写一个能够匹配正数的正则,包括没有小数点的数。

使用案例:

let reg = /your regexp/g;

let str = "1.5 0 12. 123.4.";

alert( str.match(reg) );   // 1.5, 0, 12, 123.4

\d+ 可以匹配一个整数。

\.\d+ 可以匹配小数部分。

因为小数部分不一定存在,所以我们将其放入捕获括号内,搭配量词 '?'

最终我们得到这样一个正则表达式:\d+(\.\d+)?

let reg = /\d+(\.\d+)?/g;

let str = "1.5 0 12. 123.4.";

alert( str.match(re) );   // 1.5, 0, 12, 123.4

编写一条正则表达式来查找所有的数字,包括整数、浮点数和负数。

例如:

let reg = /your regexp/g;

let str = "-1.5 0 2 -123.4.";

alert( str.match(re) ); // -1.5, 0, 2, -123.4

回顾上个问题,\d+(\.\d+)? 可以匹配一个具有可选择小数部分的正数。

那么我们只需要在最前面加上一个可选的负号 - 即可:

let reg = /-?\d+(\.\d+)?/g;

let str = "-1.5 0 2 -123.4.";

alert( str.match(reg) );   // -1.5, 0, 2, -123.4

一条算数表达式包括两个数字及其中间的一个运算符。例如:

  • 1 + 2
  • 1.2 * 3.4
  • -3 / -6
  • -2 - 2

运算符可能为:"+""-""*""/"

开头、结尾和中间可能存在额外的空格。

编写一个函数 parse(expr)。它接收一个表达式作为参数,并且返回一个包含以下三个值的数组:

  1. 第一个数。
  2. 运算符。
  3. 第二个数。

例如:

let [a, op, b] = parse("1.2 * 3.4");

alert(a); // 1.2
alert(op); // *
alert(b); // 3.4

回顾之前的问题,我们用 -?\d+(\.\d+)? 来匹配数字。

[-+*/] 匹配运算符。我们把 - 放在最前面,因为如果放在中间的话,则表示字符范围,这并不是我们想要的。

注意,在 JavaScript 中,/.../ 中的 / 需要被转义。

我们需要匹配一个数字、一个运算符,还有另一个数字。除此以外,还有它们之间可能存在的空格。

完整的正则表达式为:-?\d+(\.\d+)?\s*[-+*/]\s*-?\d+(\.\d+)?

为了将得到的结果转化为数组,我们须将所需的数据:数字及运算符,包裹在括号中,对应的表达式为:(-?\d+(\.\d+)?)\s*([-+*/])\s*(-?\d+(\.\d+)?)

实际操作:

let reg = /(-?\d+(\.\d+)?)\s*([-+*\/])\s*(-?\d+(\.\d+)?)/;

alert( "1.2 + 12".match(reg) );

结果包括:

  • result[0] == "1.2 + 12"(完整匹配)
  • result[1] == "1"(第一个捕获组)
  • result[2] == ".2"(第二个捕获组 —— 小数部分)
  • result[3] == "+"(…)
  • result[4] == "12"(…)
  • result[5] == undefined(最后一个小数部分不存在,因此为 undefined)

我们只需要数字和运算符,不需要小数部分。

因此,我们可以加上 ?: 来去除多余的捕获组,例如:(?:\.\d+)?

最终答案:

function parse(expr) {
  let reg = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/;

  let result = expr.match(reg);

  if (!result) return;
  result.shift();

  return result;
}

alert( parse("-1.23 * 3.45") );  // -1.23, *, 3.45
教程路线图

评论

在评论之前先阅读本内容…
  • 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
  • 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。
  • 如果你无法理解文章中的内容 — 请详细说明。