正規表達式是現代 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. 在匹配本身中:使用反向引用 \1\2
  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 來引用