JavaScript正则表达式与其他语言的行为差异详解

作者:木棉

时间:2026-10-30

浏览:481

本文深入解析JavaScript正则表达式在空字符类、$锚点匹配位置、点号元字符范围以及向前引用机制上与其他主流语言(如Perl、Python、Java等)的关键差异。重点说明JS中空字符类[[wp:a0]]的合法性、$在非多行模式下仅匹配字符串末尾(类似\z)的行为,以及向前引用时捕获组值在量词重新一轮匹配前被清空为[[wp:a35]]的特殊机制。

文章目录

前言

近期观察到JavaScript正则表达式在部分场景下的行为与其他编程语言或工具存在显著差异,表现较为特殊。尽管实际开发中极少需要编写此类正则,但了解这些底层行为仍有其价值。

文中代码示例均基于兼容ES5标准的JavaScript环境运行,这意味着在IE9之前的浏览器或Firefox 4等较老版本中,实际表现可能与下文描述不符。

空字符类与否定空字符类

1. 空字符类

不包含任何字符的字符类[]被称为空字符类(empty char class)。这一概念在其他语言中并不存在,因为该写法在其他语言中属于非法语法,相关文档和教程也不会介绍非法语法。以下演示其他语言或工具对该语法的报错处理:


$echo | grep '[]'
grep: Unmatched [ or [^

$echo | sed '/[]/'
sed:-e 表达式 #1,字符 4:未终止的地址正则表达式

$echo | awk '/[]/'
awk: cmd. line:1: /[]/
awk: cmd. line:1: ^ unterminated regexp
awk: cmd. line:1: error: Unmatched [ or [^: /[]//

$echo | perl -ne '/[]/'
Unmatched [ in regex; marked by <-- HERE in m/[ <-- HERE ]/ at -e line 1.

$echo | ruby -ne '/[]/'
-e:1: empty char-class: /[]/

$python -c 'import re;re.match("[]","")'
Traceback (most recent call last):
 File "<string>", line 1, in <module>
 File "E:\Python\lib\re.py", line 137, in match
 return _compile(pattern, flags).match(string)
 File "E:\Python\lib\re.py", line 244, in _compile
 raise error, v # invalid expression
sre_constants.error: unexpected end of regular expression

而在JavaScript中,空字符类是合法的正则组成部分,但其效果为“永不匹配”,即无论输入什么都会匹配失败。这相当于一个空否定正向环视(empty negative lookahead)(?!)的效果:


js> "whatever\n".match(/[]/g) //空字符类,永不匹配
null
js> "whatever\n".match(/(?!)/g) //空否定正向环视,永不匹配
null

显然,这种写法在JavaScript中几乎没有实际用途。

2. 否定空字符类

不包含任何字符的否定字符类[^]被称为否定空字符类(negative empty char class)或空否定字符类(empty negative char class)。这一术语由笔者自创,与前述空字符类类似,该写法在其他语言中同样非法:


$echo | grep '[^]'
grep: Unmatched [ or [^

$echo | sed '/[^]/'
sed:-e 表达式 #1,字符 5:未终止的地址正则表达式

$echo | awk '/[^]/'
awk: cmd. line:1: /[^]/
awk: cmd. line:1: ^ unterminated regexp
awk: cmd. line:1: error: Unmatched [ or [^: /[^]//

$echo | perl -ne '/[^]/'
Unmatched [ in regex; marked by <-- HERE in m/[ <-- HERE ^]/ at -e line 1.

$echo | ruby -ne '/[^]/'
-e:1: empty char-class: /[^]/

$python -c 'import re;re.match("[^]","")'
Traceback (most recent call last):
 File "<string>", line 1, in <module>
 File "E:\Python\lib\re.py", line 137, in match
 return _compile(pattern, flags).match(string)
 File "E:\Python\lib\re.py", line 244, in _compile
 raise error, v # invalid expression
sre_constants.error: unexpected end of regular expression
$

在JavaScript中,否定空字符类是合法的正则组成部分。其效果与空字符类相反,它可以匹配任意字符,包括换行符"\n"。也就是说,它等同于常见的[\s\S]和[\w\W]:


js> "whatever\n".match(/[^]/g)  //否定空字符类,匹配任意字符
["w", "h", "a", "t", "e", "v", "e", "r", "\n"]
js> "whatever\n".match(/[\s\S]/g)  //互补字符类,匹配任意字符
["w", "h", "a", "t", "e", "v", "e", "r", "\n"] 

需要注意的是,不能将其称为“永匹配正则”,因为字符类必须有一个字符才能进行匹配。如果目标字符串为空,或者左侧的正则已经消耗了所有字符,匹配将会失败,例如:


js> /abc[^]/.test("abc") //c后面没有字符了,匹配失败.
false

若想了解真正的“永匹配正则”,可参考笔者此前翻译的文章:“空”正则。

3. []]和[^]]

这一点的解释较为简单。在Perl及其他一些Linux命令的正则表达式中,字符类[]内若包含紧接左方括号的右方括号[]] ,则该右方括号被视为普通字符,即仅匹配"]"。而在JavaScript中,这种正则会被解析为一个空字符类后跟一个右方括号,由于空字符类不匹配任何内容.[^]],其行为也类似:在JavaScript中,它匹配的是一个任意字符(否定空字符类)后跟一个右中括号,例如"a]","b]"。而在其他语言中,匹配的是任何非"]"的字符。


$perl -e 'print "]" =~ /[]]/'
1

$js -e 'print(/[]]/.test("]"))'
false

$perl -e 'print "x" =~ /[^]]/'
1

$js -e 'print(/[^]]/.test("x"))'
false 

$锚点与点号元字符的差异

4. $锚点

部分初学者误认为$匹配的是换行符"\n",这是错误的。$是一个零宽断言(zero-width assertion),它不匹配任何实际字符,仅匹配位置。本文讨论的区别发生在非多行模式中:你可能认为在非多行模式下,$仅匹配最后一个字符后的位置。实际上并非如此,在大多数其他语言中,如果目标字符串的最后一个字符是换行符"\n",则$还会匹配该换行符之前的位置,即匹配了末尾换行符左右两侧的两个位置。许多语言提供了\Z和\z这两个表示法,若了解它们的区别,便可知晓:在其他语言(如Perl、Python、PHP、Java、C#等)中,非多行模式下的$相当于\Z;而在JavaScript中,非多行模式下的$相当于\z(仅匹配最末尾的位置,无论最后一个字符是否为换行符)。Ruby是一个特例,因其默认处于多行模式,多行模式下$会匹配每个换行符前的位置,包括结尾可能存在的换行符。余晟所著的《正则指引》一书中也阐述了这些要点。


$perl -e 'print "whatever\n" =~ s/$/替换字符/rg' //全局替换
whatever替换字符   //换行符前面的那个位置被替换
替换字符    //换行符后面的那个位置被替换

$js -e 'print("whatever\n".replace(/$/g,"替换字符"))' //全局替换
whatever
替换字符    //换行符后面的那个位置被替换

5. 点号元字符"."

在JavaScript的正则表达式中,点号元字符"."可以匹配四个行终止符(\r-回车符、\n-换行符、\u2028-行分隔符、\u2029-段落分隔符)之外的所有字符。而在其他常用语言中,仅排除换行符\n。

6. 向前引用

向前引用机制解析

众所周知,正则表达式中存在反向引用(back reference),即使用反斜杠加数字的形式引用前面某个捕获分组已匹配的字符串,目的是用于再次匹配或作为替换结果(\变为$)。但存在一种特殊情况:如果被引用的捕获分组尚未开始(以左括号为界)就使用了反向引用,会发生什么?例如正则/(\2(a)){2}/ ,(a)是第二个捕获分组,但在其左侧使用了引用其匹配结果的\2。由于正则表达式是从左向右进行匹配的,这便是本节标题“向前引用”(forwards reference)的由来,它并非一个严格的概念。那么,你认为以下JavaScript代码将返回什么结果:


js> /(\2(a)){2}/.exec("aaa")
???

在回答此问题之前,先观察其他语言中的表现。同样,在其他语言中,如此编写通常也是无效的:


$echo aaa | grep '(\2(a)){2}'
grep: Invalid back reference

$echo aaa | sed -r '/(\2(a)){2}/'
sed:-e 表达式 #1,字符 12:非法回引用

$echo aaa | awk '/(\2(a)){2}/'

$echo aaa | perl -ne 'print /(\2(a)){2}/'

$echo aaa | ruby -ne 'print $_ = ~/(\2(a)){2}/'

$python -c 'import re;print re.match("(\2(a)){2}","aaa")'
None

在awk中未报错,是因为awk不支持这种反向引用,其中的\2被解释为ASCII码为2的字符。而在Perl、Ruby、Python中未报错,具体设计原因不明(它们似乎都借鉴了Perl的设计),但效果一致,即这种情况下不可能匹配成功。

而在JavaScript中,不仅不报错,还能匹配成功,看看结果是否与你刚才的猜测一致:


js> /(\2(a)){2}/.exec("aaa")
["aa", "a", "a"]

为防止你忘记exec方法的返回值含义,此处说明一下。第一个元素是完整的匹配字符串,即RegExp["$&"] ,后续元素是每个捕获分组匹配的内容,即RegExp.$1和RegExp.$2.。为何能匹配成功?匹配过程是怎样的?我的理解如下:

首先进入第一个捕获分组(最左侧的左括号),其中第一个有效匹配项是\2。然而,此时第二个捕获分组(a)尚未轮到执行,因此RegExp.$2的值仍为undefined,所以\2匹配了目标字符串中第一个a左侧的一个空字符,或者说“位置”,这与^和其他零宽断言类似。关键在于匹配成功了。继续执行,此时第二个捕获分组(a)匹配到了目标字符串中的第一个a,RegExp.$2的值也被赋值为"a"。随后第一个捕获分组结束(最右侧的右括号),RegExp.$1的值也是"a"。接着是量词{2},意味着要从目标字符串中第一个a之后,开始进行正则(\2(a))的新的一轮匹配。这里的关键点在于:RegExp.$2的值(即\2匹配的值)是否保留第一轮匹配结束时被赋的值"a"?答案是:“不是”,RegExp.$1和RegExp.$2的值都会被清空为undefined,\1和\2又会与第一次一样,成功匹配一个空字符(相当于无效果,写不写都一样)。成功匹配了目标字符串中的第二个a,此时RegExp.$1和RegExp.$2的值再次成为"a",RegExp["$&"]的值成为完整的匹配字符串,即前两个a:“aa”。

在Firefox的早期版本(3.6)中,量词的重新一轮匹配不会清空已有捕获分组的值。也就是说,在第二轮匹配时,\2会匹配上第二个a,从而:


js> /(\2(a)){2}/.exec("aaa")
["aaa", "aa", "a"]

另外,捕获分组的结束取决于右括号是否闭合。例如/(a\1){3}/,虽然使用\1时第一个捕获分组已经开始匹配,但尚未结束,这同样属于向前引用,因此\1匹配的仍然是空:


js> /(a\1){3}/.exec("aaa")
["aaa", "a"]

再解释一个例子:


js> /(?:(f)(o)(o)|(b)(a)(r))*/.exec("foobar")
["foobar", undefined, undefined, undefined, "b", "a", "r"]

*号是量词,第一轮匹配过后:$1为"f",$2为"o",$3为"o",$4为undefined,$5为undefined,$6为undefined。

第二轮匹配开始时:捕获到的值全部重置为undefined。

第二轮匹配过后:$1为undefined,$2为undefined,$3为undefined,$4为"b",$5为"a",$6为"r"。

&被赋值为"foobar",匹配结束。

总结

以上即为总结JavaScript的正则与其他语言不同之处的全部内容,希望本文的内容对大家的学习和工作能带来帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章