总结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 相关文章推荐
JavaScript单元测试ABC
Apr 12 Javascript
jquery.post用法之type设置问题
Feb 24 Javascript
ExtJS4如何给同一个formpanel不同的url
May 02 Javascript
JavaScript通过事件代理高亮显示表格行的方法
May 27 Javascript
vue.js入门教程之计算属性
Sep 01 Javascript
javascript html5轻松实现拖动功能
Mar 01 Javascript
JavaScript数据结构之二叉树的遍历算法示例
Apr 13 Javascript
jQuery获取随机颜色的实例代码
May 21 jQuery
详解Vue 全局变量,局部变量
Apr 17 Javascript
浅谈 Webpack 如何处理图片(开发、打包、优化)
May 15 Javascript
使用webpack搭建vue环境的教程详解
Dec 31 Javascript
javascript实现拼图游戏
Jan 29 Javascript
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 采集程序 常用函数
2008/12/18 PHP
PHPUnit PHP测试框架安装方法
2011/03/23 PHP
PHP中fwrite与file_put_contents性能测试代码
2013/08/02 PHP
php利用curl抓取新浪微博内容示例
2014/04/27 PHP
Zend Framework教程之Zend_Config_Ini用法分析
2016/03/23 PHP
jquery插件 cluetip 关键词注释
2010/01/12 Javascript
jquery 图片上传按比例预览插件集合
2011/05/28 Javascript
Javascript 面向对象编程(coolshell)
2012/03/18 Javascript
再谈Jquery Ajax方法传递到action(补充)
2014/05/12 Javascript
老生常谈js动态添加事件--- 事件委托
2016/07/19 Javascript
JS实现基于拖拽改变物体大小的方法
2018/01/23 Javascript
基于Vuejs的搜索匹配功能实现方法
2018/03/03 Javascript
详解Angular5 路由传参的3种方法
2018/04/28 Javascript
vue导出html、word和pdf的实现代码
2018/07/31 Javascript
jQuery实现表格隔行换色
2018/09/01 jQuery
nodejs 使用http进行post或get请求的实例(携带cookie)
2019/01/03 NodeJs
[01:24:34]2014 DOTA2华西杯精英邀请赛5 24 DK VS LGD
2014/05/25 DOTA
python 3利用BeautifulSoup抓取div标签的方法示例
2017/05/28 Python
机器学习的框架偏向于Python的13个原因
2017/12/07 Python
Python第三方Window模块文件的几种安装方法
2018/11/22 Python
Python 获取 datax 执行结果保存到数据库的方法
2019/07/11 Python
django项目简单调取百度翻译接口的方法
2019/08/06 Python
使用Python的turtle模块画国旗
2019/09/24 Python
学python需要去培训机构吗
2020/07/01 Python
paramiko使用tail实时获取服务器的日志输出详解
2020/12/06 Python
python 实现客户端与服务端的通信
2020/12/23 Python
CSS3 Calc实现滚动条出现页面不跳动问题
2017/09/14 HTML / CSS
HTML高亮关键字的实现代码
2018/10/22 HTML / CSS
新西兰床上用品和家居用品购物网站:Adairs
2018/04/27 全球购物
马来西亚综合购物网站:Lazada马来西亚
2018/06/05 全球购物
工程造价与管理专业应届生求职信
2013/11/23 职场文书
收银员的岗位职责范本
2014/02/04 职场文书
小学生国旗下演讲稿
2014/04/25 职场文书
《谁的本领大》教后反思
2014/04/25 职场文书
世界各国短波电台对东亚播送时间频率表(SW)
2021/06/28 无线电
Logback 使用TurboFilter实现日志级别等内容的动态修改操作
2021/08/30 Java/Android