先行斷言:(?=...), (?!...)

Baseline 已廣泛支援

此特性已相當成熟,可在許多裝置和瀏覽器版本上使用。自 ⁨2015 年 7 月⁩以來,各瀏覽器均已提供此特性。

先行斷言會“向前檢視”:它試圖用給定的模式匹配後續輸入,但它不消耗任何輸入——如果匹配成功,輸入中的當前位置保持不變。

語法

正則表示式
(?=pattern)
(?!pattern)

引數

pattern

此模式可以包含任何可在正則表示式字面量中使用的內容,包括或運算子

描述

正則表示式通常從左到右匹配。這就是為什麼先行斷言和後行斷言被稱為先行和後行斷言——先行斷言判斷右側的內容,後行斷言判斷左側的內容。

為了使 (?=pattern) 斷言成功,pattern 必須匹配當前位置之後的文字,但當前位置不會改變。(?!pattern) 形式否定了斷言——如果 pattern 在當前位置不匹配,則斷言成功。

pattern 可以包含捕獲組。有關此情況下的行為的更多資訊,請參閱捕獲組頁面。

與其他正則表示式運算子不同,先行斷言不會進行回溯——此行為繼承自 Perl。這僅在 pattern 包含捕獲組,並且先行斷言之後的模式包含對這些捕獲的反向引用時才重要。例如:

js
/(?=(a+))a*b\1/.exec("baabac"); // ['aba', 'a']
// Not ['aaba', 'a']

上述模式的匹配過程如下:

  1. 先行斷言 (a+)"baabac" 中的第一個 "a" 之前成功,並且 "aa" 被捕獲,因為量詞是貪婪的。
  2. a*b 匹配 "baabac" 中的 "aab",因為先行斷言不消耗它們匹配的字串。
  3. \1 不匹配後面的字串,因為那需要兩個 "a",但只有一個可用。因此匹配器回溯,但它不進入先行斷言,所以捕獲組不能減少到一個 "a",並且整個匹配在此點失敗。
  4. exec() 在下一個位置(第二個 "a" 之前)重新嘗試匹配。這次,先行斷言匹配 "a"a*b 匹配 "ab"。反向引用 \1 匹配捕獲的 "a",匹配成功。

如果正則表示式能夠回溯到先行斷言並修改那裡的選擇,那麼匹配將在步驟 3 中透過 (a+) 匹配第一個 "a"(而不是前兩個 "a")和 a*b 匹配 "aab" 而成功,甚至無需重新嘗試下一個輸入位置。

否定先行斷言也可以包含捕獲組,但反向引用只在 pattern 內部才有意義,因為如果匹配繼續,pattern 必然是不匹配的(否則斷言會失敗)。這意味著在 pattern 之外,對否定先行斷言中那些捕獲組的反向引用總是成功。例如:

js
/(.*?)a(?!(a+)b\2c)\2(.*)/.exec("baaabaac"); // ['baaabaac', 'ba', undefined, 'abaac']

上述模式的匹配過程如下:

  1. (.*?) 模式是非貪婪的,所以它開始時不匹配任何東西。然而,下一個字元是 a,它在輸入中未能匹配 "b"
  2. (.*?) 模式匹配 "b",以便模式中的 a 匹配 "baaabaac" 中的第一個 "a"
  3. 在此位置,先行斷言成功匹配,因為如果 (a+) 匹配 "aa",那麼 (a+)b\2c 匹配 "aabaac"。這導致斷言失敗,因此匹配器回溯。
  4. (.*?) 模式匹配 "ba",以便模式中的 a 匹配 "baaabaac" 中的第二個 "a"
  5. 在此位置,先行斷言匹配失敗,因為剩餘的輸入不符合模式“任意數量的 "a"、一個 "b"、相同數量的 "a"、一個 c”。這導致斷言成功。
  6. 然而,由於斷言中沒有任何內容被匹配,\2 反向引用沒有值,因此它匹配空字串。這導致剩餘的輸入被末尾的 (.*) 消耗。

通常,斷言不能被量化。然而,在Unicode-不感知模式下,先行斷言可以被量化。這是為了網路相容性而廢棄的語法,您不應依賴它。

js
/(?=a)?b/.test("b"); // true; the lookahead is matched 0 time

示例

匹配字串而不消耗它們

有時,驗證匹配的字串後面跟著某些內容而不將它們作為結果返回很有用。以下示例匹配後面跟著逗號/句點的字串,但標點符號不包含在結果中:

js
function getFirstSubsentence(str) {
  return /^.*?(?=[,.])/.exec(str)?.[0];
}

getFirstSubsentence("Hello, world!"); // "Hello"
getFirstSubsentence("Thank you."); // "Thank you"

透過捕獲你感興趣的子匹配,也可以達到類似的效果。

模式減法和交集

使用先行斷言,你可以用不同的模式多次匹配一個字串,這允許你表達複雜的減法(是 X 但不是 Y)和交集(既是 X 也是 Y)關係。

以下示例匹配任何不是保留字識別符號(為簡潔起見,此處僅顯示三個保留字;更多保留字可以新增到此或運算子中)。[$_\p{ID_Start}][$\p{ID_Continue}]* 語法精確描述了語言規範中的識別符號字串集;你可以在詞法語法中閱讀更多關於識別符號的資訊,並在Unicode 字元類轉義中閱讀關於 \p 轉義的資訊。

js
function isValidIdentifierName(str) {
  const re = /^(?!(?:break|case|catch)$)[$_\p{ID_Start}][$\p{ID_Continue}]*$/u;
  return re.test(str);
}

isValidIdentifierName("break"); // false
isValidIdentifierName("foo"); // true
isValidIdentifierName("cases"); // true

以下示例匹配既是 ASCII 又可以用作識別符號部分的字串:

js
function isASCIIIDPart(char) {
  return /^(?=\p{ASCII}$)\p{ID_Start}$/u.test(char);
}

isASCIIIDPart("a"); // true
isASCIIIDPart("α"); // false
isASCIIIDPart(":"); // false

如果你對有限數量的字元進行交集和減法操作,你可能希望使用帶有 v 標誌啟用的字元集交集語法。

規範

規範
ECMAScript® 2026 語言規範
# prod-Assertion

瀏覽器相容性

另見