祝大家狗年旺旺
今天是新年上班的第二天了,首先在这辞鸡迎狗时刻,祝各位看到这里的朋友,新年快乐,财源滚滚。
新的一年,希望我们每个人都把握时间共同进步,争取在辞狗赢猪的时候回头看会觉得没有枉费和辜负这珍贵的时光。
前言(废话,可跳过)
鲁迅曾说过,程序员有两种,一种是会正则的,还有一种也是会正则的。如果完全不懂正则,我管你继承有几种写法,你也配做程序员?
本文就主要是记录一下javascript里RegExp对象的使用。对于正则的详细语法,可以在该页面中了解:正则可视化(没错,这是一个广告)。
通过该工具可以方便的分析正则,并且包含简单的语法列表,常用正则,以及文本测试。快点开看看吧!另外该工具的目标是和javascript支持的正则语法保持一致。
RegExp()签名
正则表达式(Regular Expression),顾名思义,就是用来描述文本的正规有规律的表达式(此定义无出处是我个人瞎说的)。先放上函数签名:
new RegExp(pattern:string , flags?:string)
对于前端童靴来说,最习以为常的一件事就是,为什么要写new?好吧,满足你,构造函数又是一个工厂函数:
RegExp(pattern:string , flags?:string)
不过最最常用的,就是字面量声明的形式了,例如: /(Reg).+(Exp).+/i
。
flags
对于flags,目前支持5种标志: g , i , m , u , y 。默认为空。
分别对应5个只读实例属性,global , ignoreCase , multiline , unicode , sticky 。 如果设置了相关的标志,则对应的属性为true。
这里,好奇心强的童靴一定要回来问我了,只读个屁,我可以重新赋值啊。
那为什么说是只读呢?可以通过该代码了解一下: Object.getOwnPropertyDescriptor(reg.__proto__,'global')
,美好又危险的弱类型。
另外还有一个只读的字符串属性 flags
,代表当前正则实例的标志。 这个你尽管试,你要是能重新赋值算我输。
lastIndex属性
对于RegExp
实例,正经属于自身的属性,只有lastIndex
,其他属性全在原型链上: Object.getOwnPropertyNames(/test/)
。
当global
或sticky
为true时,lastIndex
属性会影响正则的匹配行为,并且匹配结束后会更改实例的lastIndex
属性。
其他情况下,会忽略lastIndex
属性。即:没有g和y标志时,可以当做lastIndex
不存在。
常用方法
常用的使用正则的方法有: String#search()
, String#split()
, String#match()
, String#replace()
以及 RegExp#exec()
, RegExp#test()
。
其中 RegExp#exec()
是使用正则最强大的一个接口,返回一个匹配结果的数组,其中索引0是匹配到的子字符串,之后是匹配到的捕获组字符串。如果匹配失败返回null。RegExp#test()
和RegExp#exec()
类似,只是返回值是布尔值。
String#search()
, String#split()
, String#match()
, String#replace()
该4个方法都接受正则作为第一个参数,具体的使用就不详细说了。
对于ignoreCase,multiline,unicode标志,所有方法的匹配行为和没有标志的情况是类似的,并且不受lastIndex
属性影响,按照预期返回结果。
对于global和sticky,匹配行为会略有不同,并且和lastIndex
属性相互影响,具体情况在下文指出。
ignoreCase/multiline标志
为什么先介绍这两个,因为他娘的简单啊。这两个标志最好理解了,故名思议,就是忽略大小写和多行匹配。
var str='wwl\nabc';
/ABC/.test(str); //false
/ABC/i.test(str); //true
/^ABC/i.test(str); //false
/^ABC/im.test(str); //true
unicode标志
在ES6中,新增了两个标志,分别是sticky和unicode。
JS中处理字符串使用的是UTF-16的编码形式,一个字符串可以理解为一个16位无符号整数序列,其中的每个整数代表一个utf-16字符。而对于Unicode code point 大于0xffff的字符,会被当做32位处理,即2个字符。 (个人对于编码的理解也十分浅显)。
'🤗'.length === 2; //true
'🤗' === '\ud83e\udd17'; //true
ES6开始,添加了JS对unicode更友好的支持。例如上面这个emoji还可以这么写:
'🤗' === '\u{1f917}'; //true
对应正则,也可以使用相应的语法:
/\u{1f917}/.test('🤗'); //false
/\u{1f917}/u.test('🤗'); //true
/\u1f917/.test('🤗'); //false
/\u1f917/u.test('🤗'); //false
而在es6之前,只能这么写:
/\ud83e\udd17/.test('🤗'); //true
并且,正则表达式中的"."语法也可以正确识别出单个字符:
'🤗'.match(/./g).length; //2
'🤗'.match(/./gu).length; //1
global标志
global标志,即是否支持全局匹配。听起来很好理解,但是逻辑上怎么实现全局匹配呢?简单的说,就是和lastIndex
属性相互影响。
当存在global标志时,每次正则匹配都会从lastIndex
指定的索引处进行。
并且匹配结束后,会更新lastIndex
属性为匹配结果的下一个字符的位置,如果匹配失败则重置为0。
例如:
var reg1=/#/g, reg2=/#/;
console.log(reg1.test('reg#exp'),reg1.lastIndex); //true 4
console.log(reg1.test('reg#exp'),reg1.lastIndex); //true 0
console.log(reg2.test('reg#exp'),reg1.lastIndex); //true 0
console.log(reg2.test('reg#exp'),reg1.lastIndex); //true 0
reg1第一次test()
完成后,lastIndex
属性为4,即匹配结果"#"的下一个字符"e"的索引,第二次test()
从索引4开始匹配,则匹配失败,lastIndex
重置为0。
所以借助lastIndex
属性,global标志以及while
就可以完成全局匹配了,例如:
var str = 'wwl,Wwl,wwL,how many', reg = /wwl/ig, total = 0;
while (reg.exec(str)) total++;
console.log(total); //3
在以上列出的常用方法中,对于存在global标志时的情况,简单的列个表格:
方法 | 匹配起始位置 | 匹配成功lastIndex值 | 匹配失败lastIndex值 | 匹配成功返回值 | 匹配失败返回值 |
---|---|---|---|---|---|
RegExp#exec() | lastIndex指定处 | 匹配结果的索引+1 | 0 | 单个匹配结果的数组 | NULL |
RegExp#test() | lastIndex指定处 | 匹配结果的索引+1 | 0 | true | false |
String#replace() | 0 | 0 | 0 | 替换掉所有匹配的字符串 | 原字符串 |
String#match() | 0 | 0 | 0 | 所有匹配结果的数组 | NULL |
String#search() | 0 | 不改变 | 不改变 | 第一个匹配的索引位置 | -1 |
String#split() | 0 | 不改变 | 不改变 | 拆分后的数组 | 拆分后的数组 |
为了对比,再在放一个表格,当没有设置标志或只存在ignoreCase,multiline,unicode标志时:
方法 | 匹配起始位置 | 匹配成功lastIndex值 | 匹配失败lastIndex值 | 匹配成功返回值 | 匹配失败返回值 |
---|---|---|---|---|---|
RegExp#exec() | 0 | 不改变 | 不改变 | 单个匹配结果的数组 | NULL |
RegExp#test() | 0 | 不改变 | 不改变 | true | false |
String#replace() | 0 | 不改变 | 不改变 | 只替换掉第一个匹配的字符串 | 原字符串 |
String#match() | 0 | 不改变 | 不改变 | 单个匹配结果的数组 | NULL |
String#search() | 0 | 不改变 | 不改变 | 第一个匹配的索引位置 | -1 |
String#split() | 0 | 不改变 | 不改变 | 拆分后的数组 | 拆分后的数组 |
可以看出:
RegExp#exec()
和RegExp#test()
,当存在g标志时,会从lastIndex
指定的索引处进行匹配,并且匹配完成后会重置lastIndex
。String#replace()
在没有g标志时,只替换第一个匹配的子字符串,忽略lastIndex
;在存在g标志时,会从索引0处替换所有匹配的子字符串,且重置lastIndex
为0。String#match()
在没有g标志时,返回结果和RegExp#exec()
相同,忽略lastIndex
;当存在g标志时,返回结果为包含所有匹配结果的数组,且重置lastIndex
为0。String#search()
,String#split()
会忽略lastIndex
属性。
简单点说,总共6个方法,设置global对比未设置global:
String#search()
,String#split()
行为都一样,- 其余4个方法,都会在匹配结束后更改
lastIndex
属性 - 其中
String#replace()
和String#match()
,还会影响返回结果。
sticky标志
sticky翻译过来,就是粘性匹配。单从字面意思的看,实在不太好理解。
可以先考虑这样一个场景,假设这样一个字符串:
var str="wwl is nobody"; //fighting! to be a big man.
如果我们想匹配开头的单词,可以这么写:/^\S+/
,如果想匹配结尾的单词,可以这么写:/\S+$/
。那么,如果我们想匹配从位置索引4处开始的单词呢?
目前在javascript支持的正则语法中,没有这样匹配指定位置的语法。如果非要写,我觉得是不是可以这么写:/^.{4}(\S+)/
。
那如果通过sticky标志,就可以这么写:
var reg=/\S+/y;
reg.lastIndex=4;
reg.exec(str); //["is", index: 4]
不知道各位理解其中的意思没有,再放一个和global标志的对比:
var reg_g = /is/g, reg_y = /is/y;
reg_g.lastIndex = 1;
reg_y.lastIndex = 1;
console.log(reg_g.exec(str), reg_g.lastIndex); //["is", index: 4] 6
console.log(reg_y.exec(str), reg_y.lastIndex); //null 0
reg_g.lastIndex = 4;
reg_y.lastIndex = 4;
console.log(reg_g.exec(str), reg_g.lastIndex); //["is", index: 4] 6
console.log(reg_y.exec(str), reg_y.lastIndex); //["is", index: 4] 6
sticky标志和global标志类似,从lastIndex
指定的索引出开始查找匹配,如果匹配成功,则改变lastIndex
的值为匹配结果的索引值加1,如果匹配失败,则重置lastIndex
为0。
不同之处在于,global标志会从指定的索引处一直向后查找,直到找到为止或到达字符串末尾。而sticky标志限制只匹配指定的索引位置。
当lastIndex
为0时,等效正则语法"^"。
所以这下明白了么? 再放一个示例:
var str = 'wwl#123';
var reg1 = /#/, reg2 = /#/y;
console.log(reg1.exec(str), reg1.lastIndex); //["#", index: 3] 0
console.log(reg2.exec(str), reg2.lastIndex); //null 0
reg1.lastIndex=3;
reg2.lastIndex=3;
console.log(reg1.exec(str), reg1.lastIndex); //["#", index: 3] 3
console.log(reg2.exec(str), reg2.lastIndex); //["#", index: 3] 4
console.log(reg1.exec(str), reg1.lastIndex); //["#", index: 3] 3
console.log(reg2.exec(str), reg2.lastIndex); //null 0
第一次匹配的时候,lastIndex
默认为0,在索引0处,字符为"w",所以匹配失败。
第二次匹配的时候,lastIndex
为3,在索引3处,字符为"#",所以匹配成功,更改lastIndex
为4,
接下来继续匹配,由于索引4的位置为"1",所以匹配失败,lastIndex
重置为0。
到这里,想必应该已经明白所谓粘性匹配的规则了,这里引用一下MDN给出的定义:
sticky; matches only from the index indicated by the lastIndex property of this regular expression in the target string (and does not attempt to match from any later indexes).
我猜应该会有很多人和我有同样的疑问,新增的粘性匹配有什么使用场景呢?
我简单的搜了一下,关于粘性匹配大多都出现在一些关于词法分析的文章中,这里举个特别简单的栗子,假设有如下格式字符串:
var str='<1><2><3><5>6<7><8>'
如果我们要正确匹配每个尖括号中的内容,当遇到不规范的格式时则停止匹配:
var reg = /\s*<(\d+)>\s*/y, result = [], m;
while (m = reg.exec(str)) result.push(m[1]);
console.log(result); //[1,2,3,5]
接下来看下其余5个常用方法在设置了sticky时的情况:(此为未设置global时的情况,同时设置sticky和global见后文)
RegExp#test()
RegExp#test()
与RegExp#exec()
匹配逻辑完全相同,只是RegExp#test()
返回的是布尔值,而RegExp#exec()
返回的是数组或者null。
String#split()
对于String#split()
,会忽略sticky标志,例如:
var str = 'a,b,c,d', reg1 = /,/, reg2 = /,/y;
reg1.lastIndex = 5;
reg2.lastIndex = 5;
console.log(str.split(reg1), reg1.lastIndex); //["a", "b", "c", "d"] 5
console.log(str.split(reg2), reg2.lastIndex); //["a", "b", "c", "d"] 5
String#search()
对于String#search()
,会忽略lastIndex
,但是要保证开头位置匹配,类似正则前面添加了"^"。这个解释起来比较费劲,看下代码就明白了:
var str = 'abc';
console.log(str.search(/b/)); //1
console.log(str.search(/b/y)); //-1
console.log(str.search(/ab/y)); //0
console.log(str.search(/^ab/)); //0
String#replace()
在上面说global标志时,已经说过,当不存在global时,replace()
只替换第一个匹配的子字符串。
global是控制是否全局匹配的,所以当只设置了sticky时,仍然只替换第一个匹配的子字符串,示例代码:
var str = '1,2,3';
var reg_n = /,/, reg_y = /,/y;
console.log(str.replace(reg_n, '#'), reg_n.lastIndex); //1#2,3 0
console.log(str.replace(reg_y, '#'), reg_y.lastIndex); //1,2,3 0
reg_n.lastIndex=1;
reg_y.lastIndex=1;
console.log(str.replace(reg_n, '#'), reg_n.lastIndex); //1#2,3 1
console.log(str.replace(reg_y, '#'), reg_y.lastIndex); //1#2,3 2
reg_n.lastIndex=2;
reg_y.lastIndex=2;
console.log(str.replace(reg_n, '#'), reg_n.lastIndex); //1#2,3 2
console.log(str.replace(reg_y, '#'), reg_y.lastIndex); //1,2,3 0
可以看到,在sticky下,replace()
会仅从lastIndex
指定的索引出检查是否匹配,不会进行后续的检查。如果匹配则替换,lastIndex
为匹配结果位置加1;如果不匹配,则lastIndex
重置为0。
String#match()
在上文说过,如果不存在global时,String#match()
的返回值和RegExp#exec()
一致,即可以理解为当没有global标志时,str.match(reg)等同于reg.exec(str)。
同样列个简单的表格:
方法 | 匹配起始位置 | 匹配成功lastIndex值 | 匹配失败lastIndex值 | 匹配成功返回值 | 匹配失败返回值 |
---|---|---|---|---|---|
RegExp#exec() | 仅从lastIndex指定处 | 匹配结果的索引+1 | 0 | 单个匹配结果的数组 | NULL |
RegExp#test() | 仅从lastIndex指定处 | 匹配结果的索引+1 | 0 | true | false |
String#replace() | 仅从lastIndex指定处 | 匹配结果的索引+1 | 0 | 只替换掉第一个匹配的字符 | 原字符串 |
String#match() | 仅从lastIndex指定处 | 匹配结果的索引+1 | 0 | 单个匹配结果的数组 | NULL |
String#search() | 从字符串开头位置匹配 | 不改变 | 不改变 | 第一个匹配的索引位置 | -1 |
String#split() | 0 | 不改变 | 不改变 | 拆分后的数组 | 拆分后的数组 |
简单的说,除了split()
没什么影响,search()
限制必须匹配字符开头,其余的方法都限制仅从lastIndex
处检查匹配,并会更改lastIndex
的值。
同时设置sticky标志和global标志
上文已经总结过,当设置global标志时,只会影响4个方法: RegExp#exec()
,RegExp#test()
,String#replace()
,String#match()
。
RegExp#exec(), RegExp#test()
sticky和global都是从lastIndex
开始匹配,并且会更改lastIndex
值,针对RegExp#exec()
,sticky其实相当于更严格的global。
所以,如果同时设置了sticky和global,可以当做只设置了sticky。
String#replace()
如果设置了global,则replace()
会替换所有匹配,否则只替换第一个匹配。
如果同时设置了global和sticky呢:
var reg=/w/gy;
console.log('wwlww'.replace(reg, '#'), reg.lastIndex); //##lww 0
reg.lastIndex = 3;
console.log('wwlww'.replace(reg, '#'), reg.lastIndex); //##lww 0
这里写一个伪代码:
function replace_gy(str, searchRegex, replacement) {
var reg = new RegExp(searchRegex.source, 'y');
var result = '';
for (var i = 0; i < str.length;) {
if (reg.test(str)) {
result += replacement;
}
else {
result += str.slice(i);
break;
}
i = reg.lastIndex;
}
return result;
}
当设置了global全局匹配,就会从索引0处以粘性匹配的规则检查是否进行替换,最终达到匹配失败位置或者匹配到字符串末尾,正则的lastIndex
被重置为0。
String#match()
当没有设置global时,String#match()
等用于RegExp.exec()
,返回单个匹配结果的数组。
当设置了global时候,从索引0处会重复执行,返回所有匹配结果。
同时设置了sticky后,依然是重复执行匹配,但是是进行的粘性匹配。放个示例:
var str = 'wwl,WWL,wwL';
var reg_y = /wwl/iy, reg_g = /wwl/ig, reg_gy = /wwl/igy;
console.log(str.match(reg_y), reg_y.lastIndex); //["wwl", index: 0] 3
console.log(str.match(reg_g), reg_g.lastIndex); //["wwl", "WWL", "wwL"] 0
console.log(str.match(reg_gy), reg_gy.lastIndex); //["wwl"] 0
reg_y.lastIndex = reg_g.lastIndex = reg_gy.lastIndex = 4;
console.log(str.match(reg_y), reg_y.lastIndex); //["WWL", index: 4] 7
console.log(str.match(reg_g), reg_g.lastIndex); //["wwl", "WWL", "wwL"] 0
console.log(str.match(reg_gy), reg_gy.lastIndex); //["wwl"] 0
同样,写一个便于理解的伪代码:
function match_gy(str, searchRegex) {
var reg = new RegExp(searchRegex.source, searchRegex.flags.replace('g', ''));
var result = [], match;
for (var i = 0; i < str.length;) {
if (match = reg.exec(str)) result.push(match[0]);
else break;
i = reg.lastIndex
}
return result;
}
方法 | 匹配起始位置 | 匹配成功lastIndex值 | 匹配失败lastIndex值 | 匹配成功返回值 | 匹配失败返回值 |
---|---|---|---|---|---|
RegExp#exec() | 仅从lastIndex指定处 | 匹配结果的索引+1 | 0 | 单个匹配结果的数组 | NULL |
RegExp#test() | 仅从lastIndex指定处 | 匹配结果的索引+1 | 0 | true | false |
String#replace() | 0 | 0 | 0 | 替换掉所有匹配的字符串 | 原字符串 |
String#match() | 0 | 0 | 0 | 所有匹配结果的数组 | NULL |
Regulex
给大家推荐另一个正则可视化的页面:Regulex。在学习正则的过程中,这个工具给了我很大的帮助。
这位大神用JS实现了一个NFA,在博客中(刚试了下发现现在打不开了)看到作者目标是希望实现一个更高效的正则解析器,不过后来貌似搁浅了。有兴趣的可以围观一下。
不过在使用的过程中呢,如果对正则还不是很熟悉的时候,要不断的切换看正则语法,以及在调试界面测试字符串,于是后来就又重复造轮子,不过我的代码比较Low,全程if-else。
本人才疏学浅对正则也只是一般了解,如果文中有错误之处,还请各位及时指出,或者有什么建议也希望能够不吝赐教啊,哈哈和各位在新的一年里,共同学习。