# 正则表达式编程
什么叫知识,能指导我们实践的东西才叫知识。
学习一样东西,如果不能使用,最多只能算作纸上谈兵。正则表达式的学习,也不例外。
掌握了正则表达式的语法后,下一步,也是关键的一步,就是在真实世界中使用它。
# 正则表达式的四种操作
正则表达式是匹配模式,不管如何使用正则表达式,万变不离其宗,都需要先"匹配"。
有了匹配这一基本操作后,才有其他的操作:验证、切分、提取、替换。
进行任何相关操作,也需要宿主引擎相关API的配合使用。当然,在JavaScript中,相关API也不多。
# 验证
验证是正则表达式最直接的应用,比如表单验证。
在说验证之前,先要说清除匹配是什么概念。
所谓匹配,就是看目标字符串里是否有满足匹配的子串。因此,"匹配"的本质就是"查找"。
有没有匹配,是不是匹配上,判断是否的操作,即称为"验证"。
这里举一个例子,来看看如何使用相关API进行验证操作的。
比如,判断一个字符串中是否有数字。
使用search
:
const regex = /\d/;
const string = "abc123";
console.log(!!~string.search(regex));
// => true
使用test
:
const regex = /\d/;
const string = "abc123";
console.log(regex.test(string));
// => true
使用match
:
const regex = /\d/;
const string = "abc123";
console.log(!!string.match(regex));
// => true
使用exec
:
const regex = /\d/;
const string = "abc123";
console.log(!!regex.exec(string));
// => true
其中,最常用的是test
。
# 切分
匹配上了,我们就可以进行一些操作,比如切分。
所谓"切分",就是把目标字符串,切成一段一段的。在JavaScript中使用的是split
。
比如,目标字符串是"html,css, javascript",按逗号来切分:
const regex = /,/;
const string = "html,css,javascript";
console.log(string.split(regex));
// => ["html", "css", "javascript"]
又比如,如下的日期格式:
2017/06/26
2017.06.26
2017-06-26
可以使用split
"切出"年月日:
const regex = /\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
:
const regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
const string = "2017-06-26";
console.log(string.match(regex));
// => ["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]
使用exec
:
const regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
const string = "2017-06-26";
console.log(regex.exec(string));
// => ["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]
使用test
:
const regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
const string = "2017-06-26";
regex.test(string);
console.log(RegExp.$1, RegExp.$2, RegExp.$3);
// => "2017" "06" "26"
使用search
:
const regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
const string = "2017-06-26";
string.search(regex);
console.log(RegExp.$1, RegExp.$2, RegExp.$3);
// => "2017" "06" "26"
使用replace
:
const regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
const string = "2017-06-26";
const date = [];
string.replace(regex, function (match, year, month, day) {
date.push(year, month, day);
});
console.log(date);
// => ["2017", "06", "26"]
其中,最常用的是match
。
# 替换
找,往往不是目的,通常下一步是为了替换。在JavaScript中,使用replace
进行替换。
比如把日期格式,从yyyy-mm-dd
替换成yyyy/mm/dd
:
const string = "2017-06-26";
const today = new Date(string.replace(/-/g, "/"));
console.log(today);
// => Mon Jun 26 2017 00:00:00 GMT+0800 (中国标准时间)
这里只是简单地应用了一下replace
。但,replace
方法很是强大的,是需要重点掌握的。
# 相关API注意要点
从上面可以看出用于正则操作的方法,共有6个,字符串实例4个,正则实体2个:
String#search
String#split
String#match
String#replace
RegExp#test
RegExp#exec
# search和match的参数问题
我们知道字符串实例的那4个方法参数都支持正则和字符串。
但search
和match
,会把字符串转换为正则的。
const string = "2017.06.27";
console.log(string.search("."));
// => 0
// 需要修改成下列形式之一
console.log(string.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"
# match返回结果的格式问题
match
返回结果的格式,与正则对象是否有修饰符g
有关。
const string = "2017.06.27";
const regex1 = /\b(\d+)\b/;
const 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"]
没有g
,返回的是标准匹配格式,即,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然后是整体匹配的第一个下标,最后是输入的目标字符串。
有g
,返回的是所有匹配的内容。
当没有匹配时,不管有无g
,都返回null
。
# exec比match更强大
当正则没有g
时,使用match
返回的信息比较多。但是有g
后,就没有关键的信息index
了。
而exec
方法就能解决这个问题,它能接着上一次匹配后继续匹配:
const string = "2017.06.27";
const regex2 = /\b(\d+)\b/g;
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
// => null
// => 0
其中正则实例lastIndex
属性,表示下一次匹配开始的位置。
比如第一次匹配了"2017",开始下标时0,共4个字符,因此这次匹配结束的位置是3,下一次开始匹配的位置是4。
从上述代码看出,在使用exec
时,经常需要配合使用while
循环:
const string = "2017.06.27";
const regex2 = /\b(\d+)\b/g;
let result;
while (result = regex2.exec(string)) {
console.log(result, 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
# 修饰符g,对exec和test的影响
上面提到了正则实例的lastIndex
属性,表示尝试匹配时,从字符串的lastIndex
位开始去匹配。
字符串的四个方法,每次匹配时,都是从0开始的,即lastIndex
属性始终不变。
而正则实例的两个方法exec
、test
,当正则时全局匹配时,每一次匹配完成后,都会修改lastIndex
。下面让我们以test
为例,看看你是否会迷糊:
const 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
注意上面代码中的第三次调用test
,因为这一次尝试匹配,开始从下标lastIndex
,即3位置开始查找,自然就找不到了。
如果没有g
,自然都是从字符串第0个字符处开始尝试匹配:
const 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整体匹配时需要使用^和$
这个相对容易理解,因为test
是看目标字符串中是否有子串匹配正则,即有部分匹配即可。
如果,要整体匹配,正则前后需要添加开头和结尾:
console.log(/123/.test("a123b"));
// => true
console.log(/^123$/.test("a123b"));
// => false
console.log(/^123$/.test("123"));
// => true
# split相关注意事项
split
方法看起来不起眼,但要注意的地方有两个。
它可以有第二个参数,表示结果数组的最大长度
const string = "html,css,javascript"; console.log(string.split(/,/, 2)); // => ["html", "css"]
正则使用分组时,结果数组中时包含分隔符的
const string = "html,css,javascript"; console.log(string.split(/(,)/)); // => ["html", ",", "css", ",", "javascript"]
# replace是很强大的
《JavaScript 权威指南》认为exec
是这6个API中最强大的,而我始终认为replace
才是最强大的。因为它也能拿到该拿到的信息,然后可以假借替换之名,做些其他事情。
总体来说replace
有两种使用形式,这是因为它的第二个参数,可以是字符串,也可以是函数。
当第二个参数是字符串时,如下的字符有特殊的含义:
属性 | 描述 |
---|---|
$1,$2,···,$99 | 匹配第1-99个分组里捕获的文本 |
$& | 匹配到的子串文本 |
$` | 匹配到的子串的左边文本 |
$' | 匹配到的子串的右边文本 |
$$ | 美元符号 |
例如,把"2,3,5",变成"5=2+3":
const result = "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2");
console.log(result);
// => "5=2+3"
又例如,把"2,3,5",变成"222,333,555":
const reulst = "2,3,5".replace(/(\d+)/g, "$&$&$&");
console.log(result);
// => "222,333,555"
再例如,把"2+3=5",变成"2+3=2+3=5=5":
const result = "2+3=5".replace(/=/, "$&$`$&$`$&");
console.log(result);
// => "2+3=2+3=5=5"
我们对最后这个进行一下说明。要把"2+3=5",变成"2+3=2+3=5=5",其实就是想办法把=
替换成=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"]
此时我们可以看到replace
拿到的信息,并不比exec
少。
# 使用构造函数需要注意的问题
一般不推荐使用构造函数生成正则,而应该优先使用字面量。因为用构造函数会多写很多\
。
const string = "2017-06-27 2017.06.27 2017/06/27";
const regex = /\d{4}(-|\.|\/)\d{2}\1\d{2}/g;
console.log(string.match(regex));
// => ["2017-06-27", "2017.06.27", "2017/06/27"]
const regex1 = new RegExp("\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}", "g");
console.log(string.match(regex1));
// => ["2017-06-27", "2017.06.27", "2017/06/27"]
# 修饰符
ES5中的修饰符,共3个:
修饰符 | 描述 |
---|---|
g | 全局匹配,即找到所有匹配的,单词是global。 |
i | 忽略字母大小写,单词是ignoreCase。 |
m | 多行匹配,只影响^ 和$ ,二者变成行的概念,即行开头和行结尾。单词是multiline。 |
当然正则对象也有相应的只读属性:
const regex = /\w/img;
console.log(regex.global);
console.log(regex.ignoreCase);
console.log(regex.multiline);
// => true
// => true
// => true
# source属性
正则实例对象属性,除了global
、ignoreCase
、multiline
、lastIndex
属性之外,还有一个source
属性。
它什么时候有用呢?
比如,在构建动态的正则表达式时,可以通过查看该属性,来确认构建出的正则到底是什么:
const className = "high";
const regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
console.log(regex.source);
// => (^|\s)high(\s|$)
# 构造函数属性
构造函数的静态属性基于所执行的最近一次正则操作而变化。除了是$1,$2,···,$9
之外,还有几个不太常用的属性(有兼容性问题):
静态属性 | 描述 | 简写形式 |
---|---|---|
RegExp.input | 最近一次目标字符串 | RegExp["$_"] |
RegExp.lastMatch | 最近一次匹配的文本 | RegExp["$&"] |
RegExp.lastParen | 最近一次捕获的文本 | RegExp["$+"] |
RegExp.leftContext | 目标字符串中lastMatch之前的文本 | RegExp["$`"] |
RegExp.rightContext | 目标字符串中lastMatch之后的文本 | RegExp["$'"] |
测试代码如下:
const regex = /([abc])(\d)/g;
const 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"
# 真实案例
# 使用构造函数生成正则表达式
我们知道要优先使用字面量来创建正则,但有时正则表达式的主体是不确定的,此时可以使用构造函数来创建。模拟getElementsByClassName
方法,就是很能说明该问题的一个例子。
这里getElementsByClassName
函数的实现思路是:
- 比如要获取
className
为"high"的dom元素 - 首先生成一个正则:
/(^|\s)high(\s|$)/
- 然后再用其逐一验证页面上的所有dom元素的类名,拿到满足匹配的元素即可
代码如下(可以直接复制到本地查看运行效果):
<p class="high">1111</p>
<p class="high">2222</p>
<p>3333</p>
<script>
function getElementsByClassName (className) {
const elements = document.getElementsByTagName("*");
const regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
const result = [];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (regex.test(element.className)) {
result.push(element);
}
}
return result;
}
const highs = getElementsByClassName('high');
highs.forEach(function (item) {
item.style.color = 'red';
});
</script>
# 使用字符串保存数据
一般情况下,我们都愿意使用数组来保存数据。但我看到有的框架中,使用的却是字符串。
使用时,仍需要把字符串切分成数组。虽然不一定用到正则,但总感觉酷酷的,这里分享如下:
const 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
# if语句中使用正则替代&&
比如,模拟ready
函数,即加载完毕后再执行回调(不兼容IE的):
const readyRE = /complete|loaded|interactive/;
function ready (callback) {
if (readyRE.test(document.readyState) && document.body) {
callback();
} else {
document.addEventListener('DOMContentLoaded', function () {
callback();
}, false);
}
}
ready(function () {
alert("加载完毕!");
});
# 使用强大的replace
因为replace
方法比较强大,有时用它根本不是为了替换,只是拿其匹配到的信息做文章。
这里以查询字符串(querystring)压缩技术为例,注意下面relpace
方法中,回调函数根本没有返回任何东西。
function compress (source) {
const keys = {};
source.replace(/([^=&]+)=([^&]*)/g, function (full, key, value) {
keys[key] = (keys[key] ? keys[key] + ',' : '') + value;
});
const result = [];
for (const 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"
# 小结
相关API的注意点,基本上算是一网打尽了。
至此,恭喜你,你学完了JavaScript正则表达式的所有内容。