本章概述 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
都不相符的單一字元。
下列建構都是字元規格
來源字元比對它們自己。大多數字元都是來源字元(甚至許多在其他地方是特殊字元的字元)。只有三個字元不是
\ ] -
與往常一樣,您透過反斜線進行跳脫。如果您想要比對破折號而不跳脫它,它必須是開括號後的第 1 個字元或範圍的右側,如以下所述。
類別跳脫:先前列出的任何字元跳脫和字元類別跳脫都允許使用。還有一個額外的跳脫
\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
群組語法如下:
(«模式»)
是擷取群組。任何符合 模式
的內容都可以透過反向參照或比對作業的結果來存取。
(?:«模式»)
是非擷取群組。 模式
仍會與輸入內容比對,但不會儲存為擷取。因此,群組沒有可以參照的數字(例如透過反向參照)。
\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
?
表示比對 0 次或 1 次。
*
表示比對 0 次或多次。
+
表示比對 1 次或多次。
{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
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
)
有數種方式可以指定這兩個參數
search
字串或正規表示法
/g
旗標的正規表示法。這點出乎意料,而且是個重大陷阱。
global
旗標,否則只會嘗試比對正規表示法一次。
replacement
字串或函式
如果 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
存取群組(透過 arguments[1]
存取群組 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()
中多跳脫一些字元。