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

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

浏览器兼容性

另见