总结JavaScript的正则与其他语言的不同之处


Posted in Javascript onAugust 25, 2016

前言

最近发现JavaScript中的正则在某些地方的表现和其他语言或工具中的正则有些不同,比较另类.虽然你几乎不可能写出也几乎用不到下面我讲的这些正则,但是了解一下毕竟是好的.

本文中的代码示例都是在兼容ES5的JavaScript环境中执行的,也就是说,IE9之前版本,Fx4左右的版本,等,中的表现很有可能和我下面讲的不一样.

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.$1RegExp.$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.$1RegExp.$2的值都会被清空为undefined,\1和\2又会和第一次一样,成功匹配一个空字符(相当于无任何效果,写不写都一样).成功匹配了目标字符串中的第二个a,这时RegExp.$1RegExp.$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的正则与其他语言不同之处的全部内容,希望本文的内容对大家的学习和工作能带来帮助。

Javascript 相关文章推荐
js 异步处理进度条
Apr 01 Javascript
JavaScript 原型链学习总结
Oct 29 Javascript
JQuery设置和去除disabled属性的5种方法总结
May 16 Javascript
js+css实现增加表单可用性之提示文字
Jun 03 Javascript
JavaScript利用append添加元素报错的解决方法
Jul 01 Javascript
盘点javascript 正则表达式中 中括号的【坑】
Mar 16 Javascript
微信小程序开发之animation循环动画实现的让云朵飘效果
Jul 14 Javascript
vuejs router history 配置到iis的方法
Sep 20 Javascript
Bootstrap fileinput 上传新文件移除时触发服务器同步删除的配置
Oct 08 Javascript
JS+CSS实现随机点名(实例代码)
Nov 04 Javascript
记一次react前端项目打包优化的方法
Mar 30 Javascript
vue动态绑定style样式
Apr 20 Vue.js
js判断radiobuttonlist的选中值显示/隐藏其它模块的实现方法
Aug 25 #Javascript
jQuery实用小技巧_输入框文字获取和失去焦点的简单实例
Aug 25 #Javascript
JS实现图片延迟加载并淡入淡出效果的简单方法
Aug 25 #Javascript
JavaScript中闭包之浅析解读(必看篇)
Aug 25 #Javascript
jquery点击切换背景色的简单实例
Aug 25 #Javascript
浅析Javascript ES6中的原生Promise
Aug 25 #Javascript
微信JS接口大全
Aug 25 #Javascript
You might like
php+dbfile开发小型留言本
2006/10/09 PHP
使用PHP维护文件系统
2006/10/09 PHP
用PHP实现将GB编码转换为UTF8
2006/11/25 PHP
pw的一个放后门的方法分析
2007/10/08 PHP
zf框架的registry(注册表)使用示例
2014/03/13 PHP
php常用字符函数实例小结
2016/12/29 PHP
Laravel使用消息队列需要注意的一些问题
2017/12/13 PHP
基于jquery的分页控件(C#)
2011/01/06 Javascript
JS根据变量保存方法名并执行方法示例
2014/04/04 Javascript
jQuery中slideUp 和 slideDown 的点击事件
2015/02/26 Javascript
jQuery与JS加载事件用法分析
2016/09/04 Javascript
AngularJS中$apply方法和$watch方法用法总结
2016/12/13 Javascript
如何提高数据访问速度
2016/12/26 Javascript
详解在vue-cli项目中安装node-sass
2017/06/21 Javascript
javaScript canvas实现(画笔大小 颜色 橡皮的实例)
2017/11/28 Javascript
vue中如何让子组件修改父组件数据
2018/06/14 Javascript
Vue实现移动端页面切换效果【推荐】
2018/11/13 Javascript
基于Vue-Cli 打包自动生成/抽离相关配置文件的实现方法
2018/12/09 Javascript
Vue在 Nuxt.js 中重定向 404 页面的方法
2019/04/23 Javascript
js实现点击上传图片并设为模糊背景
2020/08/02 Javascript
JS前端基于canvas给图片添加水印
2020/11/11 Javascript
[01:44]剑指西雅图 展望TI之CIS战队专访
2014/06/25 DOTA
matplotlib设置legend图例代码示例
2017/12/19 Python
python文字转语音实现过程解析
2019/11/12 Python
python中有帮助函数吗
2020/06/19 Python
跑步爱好者一站式服务网站:Jack Rabbit
2016/09/01 全球购物
英国最大的线上保健品零售商之一:Vitamin Planet
2016/12/01 全球购物
BannerBuzz加拿大:在线定制横幅印刷、广告和标志
2020/03/10 全球购物
医学生自我鉴定范文
2014/03/26 职场文书
服务质量承诺书
2014/03/27 职场文书
预备党员公开承诺书
2014/05/28 职场文书
个人师德师风自我剖析材料
2014/09/29 职场文书
辞职信怎么写
2015/02/27 职场文书
画展观后感
2015/06/17 职场文书
学校学期工作总结
2015/08/13 职场文书
2016年“我们的节日·清明节”活动总结
2016/04/01 职场文书