什么叫知识,能指导我们实践的东西才叫知识。
学习一样东西,如果不能使用,最多只能算作纸上谈兵。正则表达式的学习,也不例外。
掌握了正则表达式的语法后,下一步,也是关键的一步,就是在真实世界中使用它。
那么如何使用正则表达式呢?有哪些关键的点呢?本章就解决这个问题。
内容包括:
正则表达式是匹配模式,不管如何使用正则表达式,万变不离其宗,都需要先“匹配”。
有了匹配这一基本操作后,才有其他的操作:验证、切分、提取、替换。
进行任何相关操作,也需要宿主引擎相关API的配合使用。当然,在JS中,相关API也不多
验证是正则表达式最直接的应用,比如表单验证。
在说验证之前,先要说清楚匹配是什么概念。
所谓匹配,就是看目标字符串里是否有满足匹配的子串。因此,“匹配”的本质就是“查找”。
有没有匹配,是不是匹配上,判断是否的操作,即称为“验证”。
这里举一个例子,来看看如何使用相关API进行验证操作的。
比如,判断一个字符串中是否有数字。
search
var regex = /\d/;
var string = "abc123";
console.log( !!~string.search(regex) ); //下面有代码讲解
// => true
讲解:可以看到 !!~string.search(regex)
的前面有!!~
,我们来说一下这个东西,首先~
是按位取反的意思(详情参考https://blog.csdn.net/qq_40241957/article/details/98850342),然后我们再来说一下!!
(详情参考我写的博客:https://blog.csdn.net/qq_40241957/article/details/98845089)
至于为什么在!!~string.search(regex)
的前面加~
,因为string.search(regex)
匹配完毕之后返回的是数字,要想让!!~string.search(regex)
等于false,只有一种可能,
那就是~string.search(regex)
等于0,而只有-1的按位非是0,其他数字都不是,
所以必须string.search(regex)
返回-1(解释:当search函数没有找到目标字符串的时候就会返回-1),正好满足我们的需求。
还有一种写法,可以把~string.search(regex)
换成(string.search(regex)+1)
,因为当search返回-1之后再加1,也是等于0。一样可以达到我们想要的效果
test
var regex = /\d/;
var string = "abc123";
console.log( regex.test(string) );
// => true
match
var regex = /\d/;
var string = "abc123";
console.log( !!string.match(regex) ); ///下面有代码讲解
// => true
讲解:string.match(regex)
返回的是存放匹配结果的数组。而!!的含义可以参考(https://blog.csdn.net/qq_40241957/article/details/98845089)
exec
var regex = /\d/;
var string = "abc123";
console.log( !!regex.exec(string) ); //下面有代码详解
// => true
讲解:regex.exec(string)返回的是存放匹配结果的数组。而!!的含义可以参考(https://blog.csdn.net/qq_40241957/article/details/98845089)
其中,最常用的是test
。
匹配上了,我们就可以进行一些操作,比如切分。
所谓“切分”,就是把目标字符串,切成一段一段的。在JS中使用的是split
。
比如,目标字符串是”html,css,javascript”,按逗号来切分:
var regex = /,/;
var string = "html,css,javascript";
console.log( string.split(regex) );
// => ["html", "css", "javascript"]
又比如,如下的日期格式:
可以使用split
“切出”年月日:
var regex = /\D/; // \D是匹配任意非数字
console.log( "2017/06/26".split(regex) );
console.log( "2017.06.26".split(regex) );
console.log( "2017-06-26".split(regex) );
// => ["2017", "06", "26"]
// => ["2017", "06", "26"]
// => ["2017", "06", "26"]
虽然整体匹配上了,但有时需要提取部分匹配的数据。
此时正则通常要使用分组引用(分组捕获)功能,还需要配合使用相关API。
这里,还是以日期为例,提取出年月日。注意下面正则中的括号:
match
var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
console.log( string.match(regex) );//下面有代码讲解
// =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]
讲解:string.match(regex)
先返回正则表达式整体匹配结果"2017-06-26",然后再返回局部括号分组里匹配到的内容
exec
var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
console.log( regex.exec(string) );//下面有代码讲解
// =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]
讲解:regex.exec(string)
先返回正则表达式整体匹配结果"2017-06-26",然后再返回局部括号分组里匹配到的内容
test
var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
regex.test(string);
console.log( RegExp.$1, RegExp.$2, RegExp.$3 ); //下面有代码详解
// => "2017" "06" "26"
讲解:RegExp.$1
取正则里的第一个括号分组里匹配到的内容, RegExp.$2
和RegExp.$3
依次是取第二个和第三个括号分组里匹配到的内容。
search
var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
string.search(regex);
console.log( RegExp.$1, RegExp.$2, RegExp.$3 ); //下面有代码详解
// => "2017" "06" "26"
讲解:RegExp.$1
取正则里的第一个括号分组里匹配到的内容, RegExp.$2
和RegExp.$3
依次是取第二个和第三个括号分组里匹配到的内容。
replace
var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
var string = "2017-06-26";
var date = [];
string.replace(regex, function(match, year, month, day) { //下面有代码讲解
date.push(year, month, day);
});
console.log(date);
// => ["2017", "06", "26"]
讲解:先拿regex匹配string,然后把regex括号分组里匹配到的内容依次赋给year, month, day,然后 date.push(year, month, day);最后在控制台输出date数组
其中,最常用的是match
。
找,往往不是目的,通常下一步是为了替换。在JS中,使用replace
进行替换。
比如把日期格式,从yyyy-mm-dd替换成yyyy/mm/dd:
var string = "2017-06-26";
var today = new Date( string.replace(/-/g, "/") ); //这里的g代表全局匹配
console.log( today );
// => Mon Jun 26 2017 00:00:00 GMT+0800 (中国标准时间)
这里只是简单地应用了一下replace
。但,replace
方法是强大的,是需要重点掌握的。
从上面可以看出用于正则操作的方法,共有6个,字符串实例4个,正则实例2个:
我们知道字符串实例的那4个方法参数都支持正则和字符串。
但search
和match
,会把字符串转换为正则的。
var string = "2017.06.27";
console.log( string.search(".") ); //search(“.”),会把这个点 当成是正则一部分,所以从开始匹配到2017的2就结束了
// => 0
//需要修改成下列形式之一
console.log( string.search("\\.") ); //search("\\.") //下面有代码讲解
console.log( string.search(/\./) );
// => 4
// => 4
console.log( string.match(".") );
// => ["2", index: 0, input: "2017.06.27"]
//需要修改成下列形式之一
console.log( string.match("\\.") );
console.log( string.match(/\./) );
// => [".", index: 4, input: "2017.06.27"]
// => [".", index: 4, input: "2017.06.27"]
console.log( string.split(".") );
// => ["2017", "06", "27"]
console.log( string.replace(".", "/") );
// => "2017/06.27"
讲解: string.search(".")
中search(“.”)
,会把这个点 当成是正则一部分,所以从开始匹配到2017的2就结束了,而string.search("\\.")
里面的\\
是对\
的转义,实质就是一个\
,然后这个\
和.
组合又对.
进行了转义。那么.
就是普通字符而不具有正则里面的.
的作用了。
match返回结果的格式,与正则对象是否有修饰符g有关。
var string = "2017.06.27";
var regex1 = /\b(\d+)\b/;
var regex2 = /\b(\d+)\b/g; //下面有代码讲解
console.log( string.match(regex1) );
console.log( string.match(regex2) );
// => ["2017", "2017", index: 0, input: "2017.06.27"]
// => ["2017", "06", "27"]
讲解:
① /\b(\d+)\b/
里面\b
代表单词边界,当 /\b(\d+)\b/
后面没有加g时,表示只匹配一次,所以当匹配到2017之后就截止了(解释:因为2017后面的.
是单词边界),而当 /\b(\d+)\b/
后面加了g
之后就是全局匹配了。所以会匹配 “2017”, “06”, “27” 单词边界都是.
②没有g
,返回的是标准匹配格式,即,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然后是整体匹配的第一个下标,最后是输入的目标字符串。
有g
,返回的是所有匹配的内容。
当没有匹配时,不管有无g
,都返回null
。
当正则没有g
时,使用match
返回的信息比较多。但是有g
后,就没有关键的信息index
了。
而exec
方法就能解决这个问题,它能接着上一次匹配后继续匹配:
var string = "2017.06.27";
var regex2 = /\b(\d+)\b/g; // \b是单词边界
console.log( regex2.exec(string) ); //下面有代码讲解
console.log( regex2.lastIndex);
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
console.log( regex2.exec(string) );
console.log( regex2.lastIndex);
// => ["2017", "2017", index: 0, input: "2017.06.27"]
// => 4
// => ["06", "06", index: 5, input: "2017.06.27"]
// => 7
// => ["27", "27", index: 8, input: "2017.06.27"]
// => 10
讲解:
①(提示:regex2.exec(string)
每次都是从lastIndex位置开始遍历的)第一次执行regex2.exec(string)
,匹配到string的“2017”,然后遇到.
就截止了。这个时候lastIndex是4,然后第二次执行regex2.exec(string)
,匹配到string的“06”,然后遇到.
就截止了,这个时候lastIndex是7。然后第三次执行regex2.exec(string)
,匹配到string的“27”截止,这个时候lastIndex是10.
②其中正则实例lastIndex
属性,表示下一次匹配开始的位置。
比如第一次匹配了“2017”,开始下标是0,共4个字符,因此这次匹配结束的位置是3,下一次开始匹配的位置是4。
从上述代码看出,在使用exec
时,经常需要配合使用while
循环:
var string = "2017.06.27";
var regex2 = /\b(\d+)\b/g;
var result;
while ( result = regex2.exec(string) ) {
console.log( result, regex2.lastIndex ); //下面的输出结果里的index是指每次匹配的起始位置
}
// => ["2017", "2017", index: 0, input: "2017.06.27"] 4
// => ["06", "06", index: 5, input: "2017.06.27"] 7
// => ["27", "27", index: 8, input: "2017.06.27"] 10
上面提到了正则实例的lastIndex
属性,表示尝试匹配时,从字符串的lastIndex
位开始去匹配。
字符串的四个方法,每次匹配时,都是从0开始的,即lastIndex
属性始终不变。
而正则实例的两个方法exec
、test
,当正则是全局匹配时,每一次匹配完成后,都会修改lastIndex
。下面让我们以test
为例,看看你是否会迷糊:
var regex = /a/g;
console.log( regex.test("a"), regex.lastIndex );
console.log( regex.test("aba"), regex.lastIndex ); //下面有代码讲解
console.log( regex.test("ababc"), regex.lastIndex );
// => true 1
// => true 3
// => false 0
讲解:regex的test每调用一次,lastIndex就会发生变化,比如第一次console.log里面调用了一次,所以,这个时候lastIndex就变成了1,然后第二次console.log调用regex.test的时候,就从1位置开始遍历匹配,匹配完后,lastIndex就变成了3,然后第三次console.log里面调用regex.test的时候就从3位置开始往后匹配,发现再也找不到"a"字符了,所以lastIndex就变成了0.
注意上面代码中的第三次调用test,因为这一次尝试匹配,开始从下标lastIndex即3位置处开始查找,自然就找不到了。
如果没有g
,自然都是从字符串第0个字符处开始尝试匹配:
var regex = /a/;
console.log( regex.test("a"), regex.lastIndex );
console.log( regex.test("aba"), regex.lastIndex );
console.log( regex.test("ababc"), regex.lastIndex );
// => true 0
// => true 0
// => true 0
这个相对容易理解,因为test
是看目标字符串中是否有子串匹配正则,即有部分匹配即可。
如果,要整体匹配,正则前后需要添加开头和结尾:
console.log( /123/.test("a123b") );
// => true
console.log( /^123$/.test("a123b") );
// => false
console.log( /^123$/.test("123") );
// => true
split
方法看起来不起眼,但要注意的地方有两个的。
第一,它可以有第二个参数,表示结果数组的最大长度:
var string = "html,css,javascript";
console.log( string.split(/,/, 2) ); 可以看到输出的结果数组里面只有2个元素,说明数组的长度为2
// =>["html", "css"]
第二,正则使用分组时,结果数组中是包含分隔符的:
var string = "html,css,javascript";
console.log( string.split(/(,)/) ); //下面有代码讲解
// =>["html", ",", "css", ",", "javascript"]
讲解:string.split(/(,)/)
里面有括号分组,所以输出结果中包含分隔符,
《JavaScript权威指南》认为exec
是这6个API中最强大的,而我始终认为replace
才是最强大的。因为它也能拿到该拿到的信息,然后可以假借替换之名,做些其他事情。
总体来说replace
有两种使用形式,这是因为它的第二个参数,可以是字符串,也可以是函数。
当第二个参数是字符串时,如下的字符有特殊的含义:
例如,把”2,3,5”,变成”5=2+3”:
var result = "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2"); //下面有代码讲解
console.log(result);
// => "5=2+3"
讲解:"2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2")
先拿字符"2,3,5"
去被正则表达式/(\d+),(\d+),(\d+)/
所匹配,然后匹配完毕之后,用$1,$2,$3拿到各括号分组里的内容,然后以 $3=$1+$2 的形式返回最终结果
又例如,把”2,3,5”,变成”222,333,555”:
var result = "2,3,5".replace(/(\d+)/g, "$&$&$&"); //下面有代码讲解
console.log(result);
// => "222,333,555"
讲解:根据上面一个图可以知道$&
代表匹配到的子串文本,我们看看 "2,3,5".replace(/(\d+)/g, "$&$&$&");
,正则/(\d+)/g
是全局匹配。拿本例来说,第一次匹配到数字2,然后因为replace函数的第二个参数是$&$&$&
,3个$&
相连, 所以就用3个2来替换。第二次匹配到3,第三次匹配到5都是一个原理。所以字符 "2,3,5"被全局替换之后就变成了 “222,333,555”
再例如,把”2+3=5”,变成”2+3=2+3=5=5”:
var result = "2+3=5".replace(/=/, "$&$`$&$'$&");//下面有代码讲解
console.log(result);
// => "2+3=2+3=5=5"
讲解:根据前面的图可以知道,
$&
代表匹配到的子串文本,
$` 代表匹配到的子串的左边文本
$’ 代表匹配到的子串的右边文本。
具体情况如下图所示
当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么:
"1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function(match, $1, $2, index, input) {
console.log([match, $1, $2, index, input]);
}); //下面有代码讲解
// => ["1234", "1", "4", 0, "1234 2345 3456"]
// => ["2345", "2", "5", 5, "1234 2345 3456"]
// => ["3456", "3", "6", 10, "1234 2345 3456"]
讲解:先看正则/(\d)\d{2}(\d)/g
,发现里面有g,代表全局匹配,拿这个正则去全局搜索匹配,而原字符串是"1234 2345 3456",其中子串“1234”,子串“2345 ”,子串“3456”都可以被正则匹配到,如下图所示
此时我们可以看到replace
拿到的信息,并不比exec
少。
一般不推荐使用构造函数生成正则,而应该优先使用字面量。因为用构造函数会多写很多\
(解释:构造函数里面主要是转义的地方太多了,所以就会出现很多的\
)。
var string = "2017-06-27 2017.06.27 2017/06/27";
var regex = /\d{4}(-|\.|\/)\d{2}\1\d{2}/g;
console.log( string.match(regex) );
// => ["2017-06-27", "2017.06.27", "2017/06/27"]
regex = new RegExp("\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}", "g"); //下面有对这段正则的详解
console.log( string.match(regex) );
// => ["2017-06-27", "2017.06.27", "2017/06/27"]
详解:RegExp("\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}", "g");
里面的\\d{4}
,其中\\
是对\
的转义,实质上是一个\
,然后这个\
于d{4}
拼接在一起就是\d{4}
,代表4位数字。
还有一个地方是\\.
,前面的两个\\
是对\
的转义,实质上是一个\
然后与.
拼接在一起就是\.
,就是对.
进行了一次转义,那么.
就只是一个普通字符,而不具有正则上的“ .
用于 匹配任意字符”的意义了
var regex = /\w/img;
console.log( regex.global ); //用于检测该regex对象是否带有g参数
console.log( regex.ignoreCase );//用于检测该regex对象是否带有i参数
console.log( regex.multiline );//用于检测该regex对象是否带有m参数
// => true
// => true
// => true
正则实例对象属性,除了global
、ingnoreCase
、multiline
、lastIndex
属性之外,还有一个source
属性。
它什么时候有用呢?
比如,在构建动态的正则表达式时,可以通过查看该属性,来确认构建出的正则到底是什么:
var className = "high";
var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
console.log( regex.source )
// => (^|\s)high(\s|$)
构造函数的静态属性基于所执行的最近一次正则操作而变化。除了是$1
,…,$9
之外,还有几个不太常用的属性(有兼容性问题):
测试代码如下:
var regex = /([abc])(\d)/g;
var string = "a1b2c3d4e5";
string.match(regex);
console.log( RegExp.input );
console.log( RegExp["$_"]);
// => "a1b2c3d4e5"
console.log( RegExp.lastMatch );
console.log( RegExp["$&"] ); //下面有代码讲解
// => "c3"
console.log( RegExp.lastParen );
console.log( RegExp["$+"] );
// => "3"
console.log( RegExp.leftContext );
console.log( RegExp["$`"] );
// => "a1b2"
console.log( RegExp.rightContext );
console.log( RegExp["$'"] );
// => "d4e5"
讲解:因为执行string.match(regex);
之后,由于正则里面有g,是全局匹配,所以就到目标字符串“a1b2c3d4e5”中去全局遍历,第一次遍历符合条件的是"a1",第二次是"b2",第三次是"c3",然后就没有了。所以console.log( RegExp.lastMatch );
打印输出的是最近一次匹配到的内容"c3",而console.log( RegExp.lastParen );
打印输出的是最近一次捕获的文本,所以是"c3"里面的"3" 。然后console.log( RegExp.leftContext );
打印输出的是RegExp.lastMatch
之前的文本,所以是 “a1b2”。 最后console.log( RegExp.rightContext );
打印输出的是RegExp.lastMatch
之后的文本。
我们知道要优先使用字面量来创建正则,但有时正则表达式的主体是不确定的,此时可以使用构造函数来创建。模拟getElementsByClassName
方法,就是很能说明该问题的一个例子。
这里getElementsByClassName
函数的实现思路是:
/(^|\s)high(\s|$)/
; (解释:(^/\s)
的意思是以开始符开头或者以空字符串开头,(\s|$)
的意思是以空字符串结尾或者以结束符结尾)1111
2222
3333
一般情况下,我们都愿意使用数组来保存数据。但我看到有的框架中,使用的却是字符串。
使用时,仍需要把字符串切分成数组。虽然不一定用到正则,但总感觉酷酷的,这里分享如下:
var utils = {};
"Boolean|Number|String|Function|Array|Date|RegExp|Object|Error".split("|").forEach(function(item) {
utils["is" + item] = function(obj) {
return {}.toString.call(obj) == "[object " + item + "]";
};
});
console.log( utils.isArray([1, 2, 3]) );
// => true
比如,模拟ready
函数,即加载完毕后再执行回调(不兼容ie的):
var readyRE = /complete|loaded|interactive/; //用“或”连接这3个字符串
function ready(callback) {
if (readyRE.test(document.readyState) && document.body) {
callback()
}
else {
document.addEventListener(
'DOMContentLoaded',
function () {
callback()
},
false
);
}
};
ready(function() {
alert("加载完毕!")
});
因为replace方法比较强大,有时用它根本不是为了替换,只是拿其匹配到的信息来做文章。
这里以查询字符串(querystring)压缩技术为例,注意下面replace
方法中,回调函数根本没有返回任何东西。
function compress(source) {
var keys = {};
source.replace(/([^=&]+)=([^&]*)/g, function(full, key, value) {
keys[key] = (keys[key] ? keys[key] + ',' : '') + value; //下面有代码讲解
});
var result = [];
for (var key in keys) {
result.push(key + '=' + keys[key]);
}
return result.join('&');
}
console.log( compress("a=1&b=2&a=3&b=4") );
// => "a=1,3&b=2,4"
本文转载自:http://blog.didispace.com/regular-expression-7/