你还不会RegExp?

517 阅读9分钟

什么是RegExp(正则表达式)

RegExp叫做正则表达式,是用于匹配字符串中字符组合的模式,更贴切的说是用自己指定的规则去检测或者修改字符串。

例如,我们想要去判断某个字符串中是否拥有某个字符,可以通过遍历这个字符串去查找,也可以通过String.prototyp上的includesindexOf或者lastIndexOf去判断,学会了正则之后就又多了一个办法;并且还可以实现精准匹配,不仅仅局限于匹配单个字符,可以进行整个字符串匹配,这不就是你想要的吗?

怎么学习正则呢?需要先了解正则的组成:元字符+修饰符=正则表达式

创建方式

正则表达式有两种创建的方式:字面量和构造函数。

  1. 字面量方式 literal/test/i,用两个/包裹起来,里面的就是正则的规则;
  2. 构造函数方式:new RegExp(rule | literal, modifier)new RegExp('test', 'i'),new RegExp(/test/, 'i')。 上面三个方式创建的正则表达式是一样的效果,构造函数方式通常用于需要使用变量来作为规则时使用。

正则的组成

元字符

元字符是正则匹配一切的源头,一切的规则都需要元字符来实现匹配,下面详细的介绍了正则中的元字符。

普通元字符,仅仅匹配这个字符,代表着匹配字符本身的含义

字符含义
/a/匹配a这个字符
/string/匹配string这几个字符

量词元字符

字符含义
?出现一次或者零次
+出现一次或者多次
*出现零次或者多次
{n}出现n次
{n,}出现n到多次
{n,m}出现n~m次

特殊元字符

字符含义
\转义字符,可以将那些特殊的元字符转译为普通的字符
.除了换行符(\n)之外的任意字符
匹配的字符串以什么开头
$匹配的字符串以什么结尾
\d匹配一个0~9的数字字符
\D匹配一个非数字字符
\w匹配一个单字字符(字母、数字或者下划线)
\W匹配一个非单字字符
\b匹配一个单词的边界
\B匹配一个非单词边界,/\B../匹配"noonday"中的'oo', 而/y\B../匹配"possibly yesterday"中的’yes‘
\f匹配一个换页符
\n匹配一个换行符
\r匹配一个回车符
\t匹配一个水平制表符
\v匹配一个垂直制表符
\s匹配一个空白字符(包括空格、制表符、换页符和换行符)
\S匹配一个非空白字符
x | y匹配x或者yz
[xyz]匹配x或者y或者z中的一个
[^xy]匹配除了x和y的任意字符
[a-z]匹配a-z这个范围中的任意字符,[0-9a-zA-Z_] === \W``
[^a-z]匹配除了a-z这个范围内的其他任意字符

以上就是正则中大部分的字符,更多的请查阅MDN官方文档

修饰符

字符含义
gglobal的缩写,代表全局匹配,即匹配完一个会继续向下匹配,会解决正则的懒惰性(下面再描述)
iignore的缩写,代表忽略大小写
mmultiline的缩写,代表多行匹配

介绍了这么多,接下来进行一些实战环节

实战小demo

  1. 匹配一个忽略大小写的单词good:/\bgood\b/i.test('good good study,day day up!');
  2. 匹配一个只包含数字和小写字母的字符串:/[0-9]a-z/.test('ac123bd');
  3. 精确匹配wonderful regexp/^wonderful regexp$/.test('wonderful regexp'); ...

[]中的cases

  1. 在[]中的特殊字符都会呈现出自己原本的作用:/[@+]/,这样会去匹配@或者+中的一个;
  2. 但是[]中的\d不会呈现自己原本的作用,依旧代表着0-9。

括号分组的概念

在正则表达式中可以通过()来进行分组捕获。

字符含义
(x)将x进行分组捕获,/\d(\d)/.exec('12')不仅仅捕获大正则,并且将()里面的小正则也捕获,捕获结果为['12', '2'];并且可以将x作为一个引用在后面使用,例如/(x)\1/,就相当于/(x)x/
(?:x)只匹配不捕获,/\d(?:\d)/.exec('12')仅仅捕获大正则,小正则不捕获,捕获结果为['12']
x(?=y)正向查找,当且仅当x后面紧跟着的是y时才能捕获到,但是y不是捕获的内容获取不到,只是用于检查,/x(?=y)/.test('xy')能捕获,/x(?=y)/.test('xwy')不能捕获
(?<=y)x反向查找,当且仅当x前面紧跟着的是y时才能捕获到,但是y不是捕获的内容获取不到,只是用于检查,/(?<=y)x/.test('yx')能捕获,/(?<=y)x/.test('ywx')不能捕获
x(?!y)正向否定查找,当且仅当x后面紧跟着的不是y时才能捕获到,但是y不是捕获的内容获取不到,只是用于检查,/x(?!y)/.test('xwy')能捕获,/x(?!y)/.test('xy')不能捕获
(?<!y)x反向否定查找,当且仅当x前面紧跟着的不是y时才能捕获到,但是y不是捕获的内容获取不到,只是用于检查,/(?<!y)x/.test('ywx')能捕获,/(?<!y)x/.test('yx')不能捕获

实战小demo

  1. 判断一个英文单词是否符合oo叠词( food,foot,boot等等 ):/(o)\1/,会成功匹配food;
  2. 查找一个符合前缀的单词( uni-add ):/(?<=uni-)add/,会成功捕获add;
  3. 查找一个符合后缀的单词( add-het ):/add(?=het)/,会成功捕获add;
  4. 查找一个符合不前缀的单词( uniu-add ):/(?<!uni-)add/,会成功捕获add;
  5. 查找一个符合不后缀的单词( add-ihet ):/add(?!het)/,会成功捕获add。

更多实战可以更具具体情况自己灵活运用哦!

正则中的匹配方法

test

RegExp.prototype.test:判断字符串是否符合正则的规则

exec

RegExp.prototype.exec:将符合正则规则的字符捕获到 捕获身份证上的出生年月:

let regexp = /\d{6}(\d{4})(\d{2})(\d{2})\d{4}/;
let target = '123456202109049901';
let result = regexp.exec(target);
// result -> ['123456202109049901','2021','09','04']
//=>['123456202109049901','2021','09','04', index: 0, input: "123456202109049901"]
//=>第一项:大正则匹配的结果
//=>其余项:每一个小分组单独匹配捕获的结果
//=>如果设置了分组(改变优先级),但是捕获的时候不需要单独捕获,可以基于?:来处理

正则匹配具有懒惰性,即当找到一个符合条件的就立即停止查找,可以通过设置修饰符global取消懒惰性。

// 下面的规则只会匹配一次2-,对于3-不会匹配到,这就是因为正则的懒惰性
/\d-/.exec('2-3-4')
// 下面添加global修饰符就会取消懒惰性,2-和3-都会匹配到
/\d-/g.exec('2-3-4')

exec只会捕获一次,就算设置了global修饰符,需求:设计一个execAll方法实现调用exec获取全部满足规则的结果

//=>需求:编写一个方法execAll,执行一次可以把所有匹配的结果捕获到(前提正则一定要设置全局修饰符g)
~ function () {
    function execAll(str = "") {
        //=>str:要匹配的字符串
        //=>this:RegExp的实例(当前操作的正则)
        //=>进来后的第一件事,是验证当前正则是否设置了G,不设置则不能在进行循环捕获了,否则会导致死循环
        if (!this.global) return this.exec(str);
        //=>ARY存储最后所有捕获的信息  RES存储每一次捕获的内容(数组)
        let ary = [],
            res = this.exec(str);
        while (res) {
            //=>把每一次捕获的内容RES[0]存放到数组中
            ary.push(res[0]);
            //=>只要捕获的内容不为NULL,则继续捕获下去
            res = this.exec(str);
        }
        return ary.length === 0 ? null : ary;
    }
    RegExp.prototype.execAll = execAll;
}();

正则还具有贪婪性

let str = "正则201@202贪婪性";
//=>正则捕获的贪婪性:默认情况下,正则捕获的时候,是按照当前正则所匹配的最长结果来获取的
let reg = /\d+/g;
console.log(str.match(reg)); //=>["201","202"]

//=>在量词元字符后面设置?:取消捕获时候的贪婪性(按照正则匹配的最短结果来获取)
reg = /\d+?/g;
console.log(str.match(reg)); //=>["2", "0", "1", "2", "0", "2"]

seasrch

String.prototype.search:在字符串中查找满足规则的结果的索引值,不存在输出-1

'abc'.search(/a/) // 0
'abc'.search(/b/) // 1
'abc'.search(/c/) // 2
'abc'.search(/d/) // -1

match

String.prototype.match:去捕获字符串中满足规则的结果,输出为一个数组 match的结果和exec一样,既能捕获大正则也能捕获到小正则中的结果,但是在全局匹配的时候,只能捕获大正则中的结果

//=>不设置g只匹配一次,exec和match获取的结果一致(既有大正则匹配的信息,也有小分组匹配的信息)
let reg = /\{(\d+)\}/;
console.log(reg.exec(str));
console.log(str.match(reg));
//["{0}", "0",...]
*/

let reg = /\{(\d+)\}/g;
//console.log(str.match(reg)); //=>["{0}", "{1}", "{2}"] 多次匹配的情况下,match只能把大正则匹配的内容获取到,小分组匹配的信息无法获取

String.prototype.matchAll

String.prototype.matchAll:全局捕获,将捕获到的结果(包括大正则和小正则)以迭代器的对象形式返回

var regexp = /t(e)(st(\d?))/g;
var str = 'test1test2';
let array = [...str.matchAll(regexp)];
array[0];
// ['test1', 'e', 'st1', '1', index: 0, input: 'test1test2', length: 4]
array[1];
// ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', length: 4]

// str.matchAll(regexp)可以使用for...of去获取结果
for(result of str.matchAll(regexp)){
    console.log(result);
}
// ['test1', 'e', 'st1', '1', index: 0, input: 'test1test2', length: 4]
// ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', length: 4]

String.prototype.split

String.prototype.split:常用于通过正则分割字符串

// 分割以,-_连接的字符串
'a,b-c_d'.split(/[,\-_]/g); // [a,b,c,d]

String.prototype.replace

String.prototype.replace:常用作实现字符串的替换,将捕获到的结果进行替换

在使用之前先学习replace的语法:

String.prototype.replace( regexp | substr , newSubStr | function )

  1. regexp:接受一个正则表达式,按照正则的规则去匹配,可以设置全局匹配;
  2. substr:接受一个字符串,有且仅有匹配一次,将匹配到的字符串片段替换为想要的newSubStr;
  3. newSubStr:用于替换的字符串,如果是正则匹配,可以使用一些特殊的变量名( $$, $&, $`, $', $n );
  4. function:可以用于接受一些参数,然后将用于替换的字符串由自己管控。

newSubStr的特殊变量

// $$ 代表插入一个$,将匹配到的结果替换为$
'aa=bb'.replace(/\w=\w/,'$$') // => 'a$b'

// $& 代表匹配到的结果
'aa=bb'.replace(/\w=\w/,'$&') // => 'aa=bb'
'aa=bb'.replace(/\w=\w/,'$&$&') // => 'aa=ba=bb'

// $` 代表匹配到的结果的左侧字符串
'aa=bb'.replace(/\w=\w/,'$`') // => 'aab'

// $' 代表匹配到的结果的右侧字符串
'aa=bb'.replace(/\w=\w/,"$'") // => 'abb'

// $n 代表括号分组小正则匹配的结果,n是正整数,将大正则匹配到的结果替换,
'aa=bb'.replace(/(\w)=(\w)/,"$1") // => 'aab'
'aa=bb'.replace(/(\w)=(\w)/,"$2") // => 'abb'
'aa=bb'.replace(/(\w)=(\w)/,"$2-$1") // => 'ab-ab'

以下有四个不错的快速熟悉正则的挑战:

验证字符出现的次数

需求:找出一个字符串中哪个字符出现的次数最多,出现了几次?

/* let str = "good good study, day day up";
let obj = {};
[].forEach.call(str, char => {
	if (typeof obj[char] !== "undefined") {
		obj[char]++;
		return;
	}
	obj[char] = 1;
});
let max = 1,
	res = [];
for (let key in obj) {
	let item = obj[key];
	item > max ? max = item : null;
}
for (let key in obj) {
	let item = obj[key];
	if (item === max) {
		res.push(key);
	}
}
console.log(`出现次数最多的字符是:${res},出现了${max}次`); */

/* let str = "good good study, day day up";
str = str.split('').sort((a, b) => a.localeCompare(b)).join('');
// console.log(str);
let reg = /([a-zA-Z])\1+/g;
let ary = str.match(reg);
// console.log(ary); 
ary.sort((a, b) => b.length - a.length);
// console.log(ary); 
let max = ary[0].length,
	res = [ary[0].substr(0, 1)];
for (let i = 1; i < ary.length; i++) {
	let item = ary[i];
	if (item.length < max) {
		break;
	}
	res.push(item.substr(0, 1));
}
console.log(`出现次数最多的字符是:${res},出现了${max}次`); */

/* let str = "good good study, day day up",
	max = 0,
	res = [],
	flag = false;
str = str.split('').sort((a, b) => a.localeCompare(b)).join('');
for (let i = str.length; i > 0; i--) {
	let reg = new RegExp("([a-zA-Z])\\1{" + (i - 1) + "}", "g");
	str.replace(reg, (content, $1) => {
		res.push($1);
		max = i;
		flag = true;
	});
	if (flag) break;
}
console.log(`出现次数最多的字符:${res},出现了${max}次`);
 */

格式化时间字符串

需求:根据模板规则:{0}->年 {1~5}->月日时分秒将时间字符串进行格式化。

~ function () {
	/*
	 * formatTime:时间字符串的格式化处理
	 *   @params
	 *     templete:[string] 我们最后期望获取日期格式的模板
	 *     模板规则:{0}->年  {1~5}->月日时分秒
	 *   @return
	 *     [string]格式化后的时间字符串
	 */
	function formatTime(templete = "{0}年{1}月{2}日 {3}时{4}分{5}秒") {
		let timeAry = this.match(/\d+/g);
		return templete.replace(/\{(\d+)\}/g, (...[, $1]) => {
			let time = timeAry[$1] || "00";
			return time.length < 2 ? "0" + time : time;
		});
	}

	/* 扩展到内置类String.prototype上 */
	["formatTime"].forEach(item => {
		String.prototype[item] = eval(item);
	});
}();

let time = "2019-8-13 16:51:3";
console.log(time.formatTime());
console.log(time.formatTime("{0}年{1}月{2}日"));
console.log(time.formatTime("{1}-{2} {3}:{4}"));
time = "2019/8/13";
console.log(time.formatTime());
console.log(time.formatTime("{0}年{1}月{2}日"));
console.log(time.formatTime("{1}-{2} {3}:{4}"));

处理问号传递的参数

需求:获取URL地址问号和面的参数信息。

~ function () {
	/* 
	 * queryURLParams:获取URL地址问号和面的参数信息(可能也包含HASH值)
	 *   @params
	 *   @return
	 *     [object]把所有问号参数信息以键值对的方式存储起来并且返回
	 */
	function queryURLParams() {
		let obj = {};
		this.replace(/([^?=&#]+)=([^?=&#]+)/g, (...[, $1, $2]) => obj[$1] = $2);
		this.replace(/#([^?=&#]+)/g, (...[, $1]) => obj['HASH'] = $1);
		return obj;
	}

	/* 扩展到内置类String.prototype上 */
	["formatTime", "queryURLParams"].forEach(item => {
		String.prototype[item] = eval(item);
	});
}();
let url = "http://www.api.cn/?lx=1&from=wx#video";
console.log(url.queryURLParams());
//=>{lx:1,from:'wx',HASH:'video'}

千分符

需求:实现一个千分符。

~ function () {
	/* 扩展到内置类String.prototype上 */
	["formatTime", "queryURLParams", "millimeter"].forEach(item => {
		String.prototype[item] = eval(item);
	});
}();

let num = "15628954"; //=>"15,628,954" 千分符
console.log(num.millimeter());
num = "112345678256874"; //=>"12,345,678,256,874"
console.log(num.millimeter());

完结

结束了,我也学废了!!!