正規表達式是現代 JavaScript 開發的必備工具,能大幅簡化字串解析與匹配的工作。本章涵蓋正規表達式的基礎語法、編譯機制、捕獲群組,以及常見問題的解決方案。
10.1 為什麼正規表達式如此強大#
正規表達式能將半螢幕的程式碼濃縮成單一語句。在主流 JavaScript 程式庫中,正規表達式被廣泛用於:
- 操作 HTML 節點字串
- 在 CSS 選擇器表達式中定位部分選擇器
- 判斷元素是否具有特定 class 名稱
- 輸入驗證
以驗證美國郵遞區號(99999-9999 格式)為例,不用正規表達式需要逐字元比對的冗長函式,而使用正規表達式只需一行:
function isThisAZipCode(candidate) {
return /^\d{5}-\d{4}$/.test(candidate);
}10.2 正規表達式複習#
建立方式#
在 JavaScript 中有兩種建立正規表達式的方式:
- 字面值語法:
const pattern = /test/; - RegExp 建構函式:
const pattern = new RegExp("test");
當 regex 在開發時已知,優先使用字面值語法;當需要在執行時期動態建構 regex 時,使用建構函式。
旗標(Flags)#
每個正規表達式可關聯五種旗標:
i:不區分大小寫g:全域匹配(匹配所有符合的實例,而非僅第一個)m:多行匹配y:黏性匹配(sticky),從上次匹配位置繼續嘗試u:啟用 Unicode 碼點跳脫(\u{...})
旗標加在字面值的結尾(如 /test/ig)或作為建構函式的第二個參數(如 new RegExp("test", "ig"))。
術語與運算子#
精確匹配:非特殊字元必須在字串中逐字出現。字元的連續排列隱含「後接」的意思。
字元類別(Character Class):
[abc]:匹配 a、b 或 c 中的任一字元[^abc]:匹配除了 a、b、c 之外的任何字元[a-m]:匹配 a 到 m 的任一字元
跳脫(Escaping):反斜線 \ 可跳脫特殊字元,使其成為字面值匹配。
起始與結束錨點:
^:匹配字串開頭$:匹配字串結尾^和$同時使用表示必須匹配整個字串
重複次數:
?:可選(出現 0 或 1 次)+:出現 1 次或多次*:出現 0 次或多次{n}:恰好 n 次{n,m}:n 到 m 次{n,}:至少 n 次
重複運算子預設是貪婪的(greedy),會盡可能多匹配。在運算子後加
?(如a+?)可改為非貪婪(nongreedy),只匹配最少所需的字元。
預定義字元類別:
| 術語 | 匹配內容 |
|---|---|
. | 任何字元(換行符除外) |
\d | 任何數字,等同 [0-9] |
\D | 非數字,等同 [^0-9] |
\w | 字母數字及底線,等同 [A-Za-z0-9_] |
\W | 非字母數字及底線 |
\s | 任何空白字元 |
\S | 非空白字元 |
\b | 單字邊界 |
\B | 非單字邊界 |
群組(Grouping):使用括號 () 將多個術語分組,同時建立捕獲(capture)。
交替(Alternation):使用管道符號 | 表達替代選項,如 /a|b/ 匹配 a 或 b。
反向引用(Backreference):用 \1、\2 等引用前面已捕獲的內容。例如 /<(\w+)>(.+)<\/\1>/ 可匹配 <strong>whatever</strong> 這樣的 XML 標籤。
10.3 編譯正規表達式#
正規表達式經歷兩個階段:編譯(compilation)和執行(execution)。
- 編譯發生在正規表達式被建立時,引擎將其解析為內部表示
- 每次建立正規表達式都會產生一個唯一的物件
預先建構和編譯正規表達式,將其儲存在變數中以供重複使用,可以獲得顯著的效能提升。這對所有複雜的正規表達式場景都適用。
使用 new RegExp() 建構函式可以在執行時期動態建構正規表達式,這在需要根據變數內容建立模式時特別有用:
const regex = new RegExp("(^|\\s)" + className + "(\\s|$)");在字串中建構正規表達式時,反斜線需要雙重跳脫(
\\s而非\s),因為反斜線同時是字串和正規表達式的跳脫字元。
10.4 捕獲匹配片段#
簡單捕獲#
使用 match 方法配合含有括號的正規表達式,可以提取字串中的特定值:
const match = transformValue.match(/translateY\(([^\)]+)\)/);
return match ? match[1] : "";match 回傳的陣列中,索引 0 是完整匹配的字串,後續索引依序是各個捕獲群組的內容。
全域匹配與區域匹配的差異#
- 區域匹配(無
g旗標):match回傳第一個匹配及其捕獲群組 - 全域匹配(有
g旗標):match回傳所有匹配的列表,但不包含各匹配的捕獲群組
若需要在全域搜尋中同時取得捕獲群組,可使用正規表達式的 exec 方法,在迴圈中重複呼叫:
const tag = /<(\/?)(\w+)([^>]*?)>/g;
let match,
num = 0;
while ((match = tag.exec(html)) !== null) {
assert(match.length === 4, "Every match finds each tag and 3 captures.");
num++;
}exec 會保留上次匹配的狀態,每次呼叫回傳下一個匹配及其捕獲。
引用捕獲#
捕獲可以在兩個地方被引用:
- 在匹配本身中:使用反向引用
\1、\2等 - 在替換字串中:使用
$1、$2、$3等語法
assert(
"fontFamily".replace(/([A-Z])/g, "-$1").toLowerCase() === "font-family",
"Convert the camelCase into dashed notation."
);非捕獲群組#
當括號僅用於分組而不需要捕獲時,使用被動子表達式(passive subexpression)語法 (?:...):
const pattern = /((?:ninja-)+)sword/;內層的 (?:ninja-) 只作為分組用途,不會產生額外的捕獲,避免不必要的效能消耗。
10.5 使用函式進行替換#
String.replace 方法最強大的功能是能以函式作為替換值。當第二個參數是函式時,該函式會為每個匹配被呼叫,參數包含:
- 完整匹配文字
- 各個捕獲群組(每個一個參數)
- 匹配在原始字串中的索引
- 原始字串
函式的回傳值作為替換結果。
範例:將 dash-case 轉為 camelCase:
function upper(all, letter) {
return letter.toUpperCase();
}
assert("border-bottom-width".replace(/-(\w)/g, upper) === "borderBottomWidth");範例:壓縮查詢字串(將 replace 作為字串遍歷工具):
function compress(source) {
const keys = {};
source.replace(/([^=&]+)=([^&]*)/g, function (full, key, value) {
keys[key] = (keys[key] ? keys[key] + "," : "") + value;
return "";
});
const result = [];
for (let key in keys) {
result.push(key + "=" + keys[key]);
}
return result.join("&");
}這裡利用 replace 的副作用來遍歷字串、提取鍵值對,而不在意替換結果本身。
10.6 解決常見問題#
匹配換行符#
JavaScript 的 . 不匹配換行符。替代方案:
/[\S\s]*/:匹配「非空白字元」或「空白字元」,即所有字元(推薦,因為簡單且效能好)/(?:.|\s)*/:使用非捕獲群組的交替匹配
匹配 Unicode#
\w 只匹配 ASCII 字母數字。要匹配包含 Unicode 字元的文字,需要擴展字元類別:
const matchAll = /[\w\u0080-\uFFFF_-]+/;從 \u0080 開始加入整個 Unicode 範圍,涵蓋基本多語言平面(BMP)中的所有字元。
匹配跳脫字元#
在 CSS 選擇器引擎等場景中,需要匹配包含跳脫字元的識別符:
const pattern = /^((\w+)|(\\.))+$/;這個表達式匹配:連續的文字字元序列、反斜線後接任意字元,或兩者的組合。
10.7 本章重點#
- 正規表達式可用字面值
/test/或RegExp建構函式建立 - 五種旗標:
i(不區分大小寫)、g(全域)、m(多行)、y(黏性)、u(Unicode 跳脫) []指定字元集合、^和$指定起始/結束、?/+/*指定重複次數、.匹配任意字元- 反斜線
\跳脫特殊字元;括號()分組術語並建立捕獲;管道|表示交替 match函式回傳匹配結果及捕獲,replace函式可基於模式而非固定字串進行替換- 成功匹配的部分可用反向引用
\1、\2來引用