js中常见的正则表达式总结

3,195 阅读14分钟

何为正则表达式?

正则表达式? 这里套用百度的概念

正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。 许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在Perl中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由Unix中的工具软件(例如sed和grep)普及开的。正则表达式通常缩写成“regex”,单数有regexp、regex,复数有regexps、regexes、regexen。

正则表达式构造函数

在ES5中,RegExp 构造函数的参数有两种情况。

第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。

var regex = new RegExp('xyz', 'i');
// 等价于
var regex = /xyz/i;

第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。

var regex = new RegExp(/xyz/i);
// 等价于
var regex = /xyz/i;

但是,ES5不允许此时使用第二个参数,添加修饰符,否则会报错。

var regex = new RegExp(/xyz/, 'i');
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another

ES6改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。

new RegExp(/abc/ig, 'i').flags
// "i"
上面代码中,原有正则对象的修饰符是ig,它会被第二个参数i覆盖。

正则表达式的基本语法

开始之前,先简单回顾一边js中正则表达式的一个基本概念,这部分很基础,同样也很容易让人忽视,或者说遗漏,但是却又是与正则相关的日常开发中,非常重要的概念。

基本元字符

写法 含义
. 匹配除了换行符之外的任何单个字符
\ 在非特殊字符之前的反斜杠表示下一个字符是特殊的,不能从字面上解释。例如,没有前\的'b'通常匹配小写'b',无论它们出现在哪里。如果加了'',这个字符变成了一个特殊意义的字符,反斜杠也可以将其后的特殊字符,转义为字面量
| 逻辑或操作符
[] 定义一个字符集合,匹配字符集合中的一个字符,在字符集合里面像 .,\这些字符都表示其本身
[^] 对上面一个集合取非
- 定义一个区间,例如[A-Z],其首尾字符在 ASCII 字符集里面

数量元字符

写法 含义
{m,n} 匹配前面一个字符至少 m 次至多 n 次重复,还有{m}表示匹配 m 次,{m,}表示至少 m 次
+ 匹配前面一个表达式一次或者多次,相当于 {1,},记忆方式追加(+),起码得有一次
* 匹配前面一个表达式零次或者多次,相当于 {0,},记忆方式乘法(*),可以一次都没有
? 单独使用匹配前面一个表达式零次或者一次,相当于 {0,1},记忆方式,有吗?,有(1)或者没有(1),如果跟在任何量词*,+,?,{}后面的时候将会使量词变为非贪婪模式(尽量匹配少的字符),默认是使用贪婪模式。比如对 "123abc" 应用 /\d+/ 将会返回 "123",如果使用 /\d+?/,那么就只会匹配到 "1"。

特殊元字符

写法 含义
\d [0-9],表示一位数字,记忆方式 digit
\D [^0-9],表示一位非数字
\s [\t\v\n\r\f],表示空白符,包括空格,水平制表符(\t),垂直制表符(\v),换行符(\n),回车符(\r),换页符(\f),记忆方式 space character
\S [^\t\v\n\r\f],表示非空白符
\w [0-9a-zA-Z],表示数字大小写字母和下划线,记忆方式 word
\W [^0-9a-zA-Z],表示非单词字符

标志字符

写法 含义
g 全局搜索 记忆方式global
i 不区分大小写 记忆方式 ignore
m 多行搜索

js中与正则相关的一些方法

说到正则,首先脑海中考虑到的,可能就是字符串相关的一些东西,那么很自然地,也就想到了我们处理字符串的时候所使用的一些方法:

String中的方法

search 接受一个正则作为入参,如果入参不是正则表达式,则会隐式的使用 new RegExp(obj)将其转换成一个正则!返回匹配到子串的起始位置,匹配不到返回-1(这点类似于数组中的indexOf方法)

match接受参数和上面的方法一致。返回值是依赖传入的正则是否包含 g ,如果没有 g 标识,那么 match 方法对 string 做一次匹配,如果没有找到任何匹配的文本时,match 会返回 null ,否则,会返回一个数组,数组第 0 个元素包含匹配到的文本,其余元素放的是正则捕获的文本,数组还包含两个对象,index 表示匹配文本在字符串中的位置,input 表示被解析的原始字符串。如果有 g 标识,则返回一个数组,包含每一次的匹配结果。

replace 接受两个参数,第一个是要被替换的文本,可以是正则也可以是字符串,如果是字符串的时候不会被转换成正则,而是作为检索的直接量文本。第二个是替换成的文本,可以是字符串或者函数,字符串可以使用一些特殊的变量来替代前面捕获到的子串。

? 插入一个 ‘$’

$& 插入匹配的子串。

$ ` 插入当前匹配的子串左边的内容。

$' 插入当前匹配的子串右边的内容。

$n 假如第一个参数是 RegExp对象,并且 n 是个小于100的非负整数,那么插入第 n 个括号匹配的字符串。

如果第二个参数是函数,该函数的入参如下:

match: 匹配的子串。(对应于上述的$&。)

p1,p2,...: 假如replace()方法的第一个参数是一个RegExp 对象,则代表第n个括号匹配的字符串。(对应于上述的$1,$2等。)

offset: 匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是“abcd”,匹配到的子字符串是“bc”,那么这个参数将是1)

string 被匹配的原字符串。

RegExp中的方法

test 接受一个字符串参数,如果正则表达式与指定的字符串匹配返回 true 否则返回 false

exec 同样接受一个字符串为参数,返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。匹配时,返回值跟 match 方法没有 g 标识时是一样的。数组第 0 个表示与正则相匹配的文本,后面 n 个是对应的 n 个捕获的文本,最后两个是对象 index 和 input同时它会在正则实例的 lastIndex 属性指定的字符处开始检索字符串 string。当 exec() 找到了与表达式相匹配的文本时,在匹配后,它将把正则实例的 lastIndex 属性设置为匹配文本的最后一个字符的下一个位置。有没有 g 标识对单词执行 exec 方法是没有影响的,只是有 g 标识的时候可以反复调用 exec() 方法来遍历字符串中的所有匹配文本。当 exec() 再也找不到匹配的文本时,它将返回 null,并把 lastIndex 属性重置为 0。

多个数匹配

{m,n} 表示匹配m到n个

let reg = /abcd{1,3}efg/g
let str = 'abcddefg  abcddddefg abcdefg abcdddefg'
str.match(reg) 
//返回结果 ["abcddefg", "abcdefg", "abcdddefg"]

在正则的匹配当中,一切匹配都是默认开启了贪婪模式:

其实贪婪和惰性很容易理解,从字面意思我们就可以知道,所谓的"贪婪"的意思就是,如果符合要求就一直往后匹配,一直到无法匹配为止,这就是贪婪模式。与之相对应的就是惰性模式(也可以称之为非贪婪模式),就是一旦匹配到合适的就结束,不在继续匹配下去了。

贪婪模式的标示符:+,?,*,{n},{n,},{n,m}.惰性模式:+?,??,*??,{n}?,{n,}?,{n,m}?;

非贪婪模式:

var regex = /\d{2,5}?/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) ); 
// => ["12", "12", "34", "12", "34", "12", "34", "56"]

本案例的含义就是,匹配到了2个,就不再匹配更多了。

多种情况匹配

1.主要使用[]元字符来处理,如:

let reg = /demo[1234].js/g
let filesName = 'demo1.js demo2.js demo3.js demo4.js demo5.js'
filesName.match(reg)
// => ["demo1.js", "demo2.js", "demo3.js", "demo4.js"]

2.表示范围 如果字符组里面字符特别多的话可以用-来表示范围,比如[123456abcdefGHIJKLM],可以写成[1-6a-fG-M],用[^0-9]表示'非',也就是除了数字以外的字符

3.多种情况还可以是多种分支,用管道符来连接|,比如

var regex = /good|goodbye/g;
var string = "goodbye";
console.log( string.match(regex) ); // ["good"]
// 最多保留2位小数的数字
/^([1-9]\d*|0)(\.\d{1,2})?$/
电话号码
/(\+86)?1\d{10}/
身份证
/^(\d{15}|\d{17}([xX]|\d))$/

位置相关

^,$ 匹配字符的开头和结尾,相当于是两个占位符,分别占据了开头和结尾,比如/^kingdee/ 匹配一个字符串,条件: 字符串开头的位置,紧接着是 k 然后是 i,n,g,d,e,e, 最后是字符串结尾的位置。为了验证^,$本质上是占位符,我们可以用一个案例来表现:

let str = 'kingdee';
str.replace(/^|$/g,'@');
结果为:@kingdee@ // 我们将一头一为的'字符串'替换成了@符号

/b,/B 匹配单词边界和非单词边界,单词边界具体指 \w([a-zA-Z0-9_])\W 之间的位置,包括\w^ 以及 $之间的位置,例如:

let str = 'kingdee fly [js]_reg.exp-01'.replace(/\b/g, '@')
结果是 @kingdee@ @fly@ [@js@]@_reg@.@exp@-@01@

(?=p),(?!p) 匹配 p 前面的位置(后面跟着p的那个字符的位置)和不是 p 前面位置(后面没跟着p的那个字符的位置),例如:

'hello'.replace(/(?=l)/g, '#') 结果是 he#l#lo
'hello'.replace(/(?!l)/g, '#') 结果是 #h#ell#o#

字符串位置的特性 其实字符串之间的‘空间’可以认为是无限多个的,比如说,``

字符与字符之间的位置可以是多个,甚至可以认为是无限多个空字符串,比如hello 可以是一般的 '' + 'h' + 'e' + 'l' + 'l' + 'o' + '',也可以是 '' + '' + '' + '' + 'h' + 'e' + 'l' + 'l' + 'o' + '',所以/^h\Be\Bl\Bl\Bo$/.test('hello')结果是 true/^^^^^^h\B\B\Be\Bl\Bl\Bo?$/.test('hello') 结果也是 true

案例: 千分位转换 虽然我们可以使用诸如toLocaleString等方法来直接将一串数字转为千分位格式,但是,一个问题多种解决方案,总是更好的。

let num = '1008610086';
let reg = /(?=(\d{3})+$)/g
str.replace(reg,',')
结果为: "1,008,610,086"

圆括号的使用

使用括号来表示正则中的一个局部整体的概念。主要使用圆括号。

let str = 'hello word';
let reg = /^hello (world|today)$/
str.match(reg)
结果输出:["hello world", "world", index: 0, input: "hello world", groups: undefined]
// 返回的数组中的第二个元素也就是匹配到的那个分组

分组的使用

分组在正则的使用当中可以起到很重要的作用,适应多个场景,之前的千分位转换案例中,其实已经使用了分组,也就是利用了分组来处理'重复出现'的字符串片段。

匹配并捕获

例如:日期的提取 正则表达式上可以写成/(\d{4})\/(\d{1,2})\/(\d{1,2})/,提取日期2020/4/15中的年月日。

let str = '2020/4/15'
let reg = /(\d{4})\/(\d{1,2})\/(\d{1,2})/
str.match(reg)

结果输出: ["2020/4/15", "2020", "4", "15", index: 0, input: "2020/4/15", groups: undefined]
// 可以看出,返回数组的第二项开始,分别是年月日

针对分组的情况,其实我们还可以利用RegExp对象本身的test方法,接上面的案例 :

let str = '2020/4/15'
let reg = /(\d{4})\/(\d{1,2})\/(\d{1,2})/
// str.match(reg)
reg.test(str)
// 分别获取年月日的数字
RegExp.$1
"2020"
RegExp.$2
"4"
RegExp.$3
"15"

匹配更换

比如,想把yyyy-mm-dd格式,替换成mm/dd/yyyy怎么做?

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result); 
// => "06/12/2017"
其中replace中的,第二个参数里用$1$2$3指代相应的分组。等价于如下的形式:var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function() {
	return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result); 
// => "06/12/2017"
也等价于:var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function(match, year, month, day) {
	return month + "/" + day + "/" + year;
});
console.log(result); 
// => "06/12/2017"

当然es6中提供了更加便利的日期匹配方式 具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const matchObj = RE_DATE.exec('2020-4-15');
const year = matchObj.groups.year; // 2020
const month = matchObj.groups.month; // 4
const day = matchObj.groups.day; // 15

分组的嵌套关系问题

我们开发中经常遇到分组中的分组,也就是括号中的括号,诸如此类:/(a)((a|b)+)c/,那么此类正则,该如何去理解,正则会怎么去匹配? 接上面案例中的RegExp.$1,RegExp.$2,RegExp.$3。其实这里的$1~$9是正则表达式预定义的静态属性,通过RegExp.$1引用,这里顺便就此理一下正则表达式的一个嵌套规律问题。 看以下这个简单的案例:

var reg = /(A+)((B|C|D)+)(E+)/gi;//该正则表达式有4个分组
//对应关系
//RegExp.$1 <-> (A+) // 分组1
//RegExp.$2 <-> ((B|C|D)+) // 分组2
//RegExp.$3 <-> (B|C|D) // 分组2-1 (注意这个是分组2的子分组)
//RegExp.$4 <-> (E+) // 分组3
reg.exec(str,'i');
结果输出: ["ABCDE", "A", "BCD", "D", "E"]

总结:分组的嵌套规律就是,先匹配大分组,如果大组下还有小组,那就先匹配小组。

反向引用

接之前日期的案例。 我们在日常工作中经常遇到日期转换格式问题,具体就是xxxx-xx-xx 还是 xxxx/xx/xx这个问题。 如果此时有个需求,需要判断返回的日期格式是否正确,或者说要匹配日期。 那么,作为匹配依据的正则就需要更加严谨。 例如: 后台返回的日期格式可能是2020-4-15,也可能是2020/4/15。那么如何统一匹配?可能我们能首先想到/(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})/,乍一看没问题,但是如果后台有一天返回2020-4/15,这种格式呢,这是否也能匹配上? 这样就产生了bug:

let reg = /(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})/
let string1 = "2020-04-15";
let string2 = "2020/04/15";
let string3 = "2020/04-15";
let string4 = "2020-04/15";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // true

其实想下,我们的需求很简单,就是'年'和'月'之间,'月'和'日‘ 之间的分隔字符必须是相同而且符合我们的预期的! 所以这里就要用到反向引用

let reg = /(\d{4})([\/-])(\d{1,2})\2(\d{1,2})/
let string1 = "2020-04-15";
let string2 = "2020/04/15";
let string3 = "2020/04-15";
let string4 = "2020-04/15";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // false
console.log( regex.test(string4) ); // false

非捕获匹配

之前文中出现的分组,都会捕获它们匹配到的数据,以便后续引用,因此也称他们是捕获型分组。如果只想要括号最原始的功能,但不会引用它,即,既不在API里引用,也不在正则里反向引用。此时可以使用非捕获分组(?:p),例如本文第一个例子可以修改为:

var reg = /(?:ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(reg) ); 
// => ["abab", "ab", "ababab"]

正向前瞻

表达式 名称 描述
(?=exp) 正向前瞻 匹配后面满足表达式exp的位置
(?!exp) 负向前瞻 匹配后面不满足表达式exp的位置
(?<=exp) 正向后瞻 匹配前面满足表达式exp的位置(JS不支持)
(?<!exp) 负向后瞻 匹配前面不满足表达式exp的位置(JS不支持)
var str = 'Hello, Hi, I am Hilary.';
var reg = /H(?=i)/g;
var newStr = str.replace(reg, "T");
console.log(newStr);//Hello, Ti, I am Tilary.

在这个DEMO中我们可以看出正向前瞻的作用,同样是字符"H",但是只匹配"H"后面紧跟"i"的"H"。才能够被匹配到。 那么负向前瞻呢?道理是相同的:

var str = 'Hello, Hi, I am Hilary.';
var reg = /H(?!i)/g;
var newStr = str.replace(reg, "T");
console.log(newStr);//Tello, Hi, I am Hilary.

在这个DEMO中,我们把之前的正向前瞻换成了负向前瞻。这个正则的意思就是,匹配"H",且后面不能跟着一个"i"。其实就是负向前瞻的否定模式。

常用的正则表达式

说了这则的一些基础知识,现在列举一些常用的前端正则表达式

手机号码

var mPattern = /^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\d{8}$/;

身份证号(18位)正则

var cP = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;

上述正则只能对身份证号进行基本的通过性判定,关于公民身份号码判定的更多内容可参见文档:公民身份号码正确性判定及程序实现

手机号码

var mPattern = /^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\d{8}$/;

url

var urlP= /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;

IP地址

// IPv4地址正则
var ipP = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;

// IPv6地址正则
var pattern = /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/;

QQ号正则,5至11位

var qqPattern = /^[1-9][0-9]{4,10}$/;

微信号正则,6至20位,以字母开头,字母,数字,减号,下划线

var wxPattern = /^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$/;

车牌号正则

var cPattern = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/;

包含中文正则

var cnPattern = /[\u4E00-\u9FA5]/;

中国邮编(中国邮政编码为6位数字)

var pattern = /^[1-9]\d{5}(?!\d)$/

空白行的正则表达式

var pattern = /\n\s*\r/ (可以用来删除空白行)

是否为固话

var pattern = /0\d{2,3}-\d{7,8}/

合法邮箱

var pattern = /^([a-zA-Z0-9]+[-_\.]?)+@[a-zA-Z0-9]+\.[a-z]+$/

域名

var pattern =[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?