模式的一部分可以用括號 (...)
括起來。這稱為「擷取群組」。
這有兩個效果
- 它允許將匹配的一部分作為結果陣列中的單獨項目取得。
- 如果我們在括號後放置一個量詞,它將套用於整個括號。
範例
讓我們看看括號在範例中如何運作。
範例: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]
這個正規表示式並不完美,但大多數情況下都能運作,有助於修正意外的錯別字。唯一真正可靠的電子郵件檢查方式,只能透過寄送信件來完成。
比對中的括號內容
括號從左到右編號。搜尋引擎會記住每個括號比對到的內容,並允許在結果中取得它。
如果 regexp
沒有旗標 g
,str.match(regexp)
方法會尋找第一個比對,並將它作為陣列傳回
- 在索引
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
是一種新的方法,可能需要多重載入舊瀏覽器不支援 matchAll
方法。
可能需要多重載入,例如 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
,而會回傳一個空的 iterable 物件。
例如
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
。有關偽陣列和 iterable 的更多詳細資訊,請參閱文章 Iterable。
如果我們要對結果進行迴圈,則不需要 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
回傳的相符項目,其格式與沒有旗標 g
的 match
回傳的格式相同:它是一個包含其他屬性 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
的結果是 iterable 物件,而不是陣列?為什麼方法會這樣設計?原因很簡單,就是為了最佳化。
呼叫 matchAll
並不會執行搜尋。相反地,它會回傳一個 iterable 物件,一開始沒有結果。搜尋會在每次我們對其進行迭代時執行,例如在迴圈中。
因此,只會找到所需數量的結果,不會更多。
例如,文字中可能有 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
中取得。
我們也可以在 str.replace
中的替換字串中使用括號內容:透過編號 $n
或名稱 $<name>
。
可以在開始處新增 ?:
來將群組排除在編號之外。當我們需要將量詞套用於整個群組,但不想將其作為結果陣列中的單獨項目時,會使用這種方式。我們也不能在替換字串中參照此類括號。
留言
<code>
標籤,要插入多行,請用<pre>
標籤包覆,要插入超過 10 行,請使用沙盒 (plnkr、jsbin、codepen…)