RegExp基础语法

引入

正则表达式(Regular Expression)是一门简单语言的语法规范,是强大、便捷、高效的文本处理工具

JavaScript中的正则表达式用RegExp对象表示,由两种写法:字面量写法和构造函数写法

字面量写法

正则表达式字面量定义为包含在一对斜杠/之间的字符,并且可以设置3个标志

var expression = /pattern/flags;

正则表达式的匹配模式支持下列3个标志:

g:表示全局(global)模式,即模式将被应用于所有字符串,而并非在发现第一个匹配项时立即停止

i:表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写

m:表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项

构造函数写法

和普通的内置对象一样,RegExp正则表达式对象也支持new RegExp()构造函数的形式

RegExp构造函数接收两个参数:要匹配的字符串模式(pattern)和可选的标志字符串(flags),标志字符串和字面量的三个标志含义相同:'g'、'i'、'm'

var expression = new RegExp(pattern[, flags]);
// 匹配字符串所有'at'的实例
var p1 = /at/g
// 同上
var p2 = new RegExp('at','g');

RegExp构造函数的两个参数都是字符串,使用字面量形式定义的任何表达式都可使用构造函数

元字符

大部分字符在正则表达式中,就是字面的含义,比如/a/匹配a,/b/匹配b。但还有一些字符,它们除了字面意思外,还有着特殊的含义,这些字符就是元字符

.            点号       单个任意字符(除回车符\r、换行符\n、行分割符\u2028和段分隔符\u2029外)

[]           字符组      字符组内列出的单个任意字符
[^]          排除字符组  字符组内未列出的单个任意字符

?           问号        匹配0次或一次
*            星号        匹配0次或多次
+            加号        匹配1次或多次
{min, max}   区间量词    匹配至少min次,至多max次

^            脱字符      行的起始位置
$            美元符      行的结束位置
|            竖线        分隔两边的任意一个表达式
()           括号        分组和引用,限制多选结构的范围
\1, \2...    反向引用    匹配之前的第一、第二...组括号内的表达式匹配的文本

转义字符

转义字符(escape)表示为反斜线加字符的形式,有以下3种情况

  1. 因为元字符本身有特殊的含义,所以无法直接匹配。如果要匹配它们本身,则需要对他们进行转义
// 匹配字符串a+a
console.log(/a+a/.test('a+a')); //false
console.log(/a\+a/.test('a+a')); //true
  1. 反斜线加非元字符,表示一些不能打印的特殊字符
\0        NUL字符\u0000
[\b]      匹配退格符\u0008,不要与\b混淆
\t        制表符\u0009
\n        换行符\u000A
\v        垂直制表符\u000B
\f        换页符\u000C
\r        回车符\u000D
\xnn      由十六进制数nn指定的拉丁字符
\uxxxx    由十六进制数xxxx指定的Unicode字符(\u4e00-\u9fa5代表中文)  
\cX       控制字符^X,表示ctrl-[X],其中的X是A-Z之中任一个英文字母,用来匹配控制字符
console.log(/\n/.test('hello\nworld')); // true
  1. 反斜杠加任意其他字符,默认情况就是匹配此字符,也就是说,反斜线\被忽略了
console.log(/\a/.test('a')); // true
console.log(/a/.test('a')); // true

双重转义

由于RegExp构造函数的参数是字符串,所以某些情况下,需要对字符进行双重转义。双重转义用两个反斜杠\\表示

// 匹配.at
var p1 = /\.at/;
// 等价于
var p2 = new RegExp('\\.at');

字符组

字符组(Character Class),就是指用方括号表示的一组字符,它匹配若干字符之一

// 匹配0-9这10个数字之一
var p = /[0123456789]/;
p.test('1'); // true
p.test('a'); // false

字符组中的字符排列顺序并不影响字符组的功能,出现重复字符也不会影响

/[0123456789]/
//等价于
/[9876543210]/
//等价于
/[1234567890123456789]/

正则表达式通过连字符(-)提供了范围表示法,可以简化字符组

/[0123456789]/
//等价于
/[0-9]/

/[abcdefghijklmnopqrstuvwxyz]/
//等价于
/[a-z]/

连字符(-)表示的范围是根据ASCII编码的码值来确定的,码值小的在前,码值大的在后。所以[0-9]是合法的,而[9-0]会报错

// 匹配0-9这10个数字之一
var p1 = /[0-9]/;
p1.test('1'); // true
var p2 = /[9-0]/; // 报错
p2.test('1');

字符组中可以同时并列多个'-'范围

// 匹配数字、大写字母和小写字母
var p = /[0-9A-Za-z]/
console.log(p.test('d')); // true

只有在字符组内部,连字符'-'才是元字符,表示一个范围,否则它就只能匹配普通的连字符号。另外,如果连字符出现在字符组的开头或末尾,它表示的也是普通的连字符号,而不是一个范围

console.log(/-/.test('-'));//true
console.log(/[-]/.test('-'));//true
console.log(/[0-9-]/.test('-'));//true

console.log(/[0-9]/.test('-'));//false

排除字符组

在字符组的左方括号后紧跟一个脱字符'^',表示在当前位置匹配一个没有列出的字符

// 匹配第一个是数字字符,第二个不是数字字符的字符串
console.log(/[0-9][^0-9]/.test('1e'));//true
console.log(/[0-9][^0-9]/.test('11'));//false

注意: 在字符组内部,脱字符表示排除,而在字符组外部,脱字符表示行起始位置

如果字符组中^符号不紧挨着左方括号,表示的就是其本身含义。

//匹配abc和^符号中的一个
console.log(/[a-c^]/.test('^'));// true

简记

用[0-9]、[^0-9]等字符组,可以很方便地表示数字字符和非数字字符。对于这类常用字符组,正则表达式提供了更简单的记法,这就是字符组简记(shorthands)

常见的字符组简记有\d、\w、\s。其中d表示(digit)数字,w表示(word)单词,s表示(space)空白

正则表达式也提供了对应排除型字符组的简记法:\D、\W、\S。字母完全相同,只是改为大写,这些简记法匹配的字符互补

\d     数字,等同于[0-9]
\D     非数字,等同于[^0-9]
\s     空白字符,等同于[\f\n\r\t\u000B\u0020\u00A0\u2028\u2029]
\S     非空白字符,等同于[^\f\n\r\t\u000B\u0020\u00A0\u2028\u2029]
\w     字母、数字、下划线,等同于[0-9A-z_](汉字不属于\w)
\W     非字母、数字、下划线,等同于[^0-9A-z_]

注意: \w不仅包括字母、数字,还包括下划线。所以只允许输入字母和数字时,不可以使用\w,而应该使用[0-9A-Za-z]

console.log(/\w/.test('_')); // true
console.log(/[0-9A-Za-z]/.test('_')); // false

点号

人们一般认为点号可以代表任意字符,其实并不是。.点号代表除回车(\r)、换行(\n) 、行分隔符(\u2028)和段分隔符(\u2029)以外的任意字符。

如果要表示任意字符,可以妥善的利用互补属性。比如,[\s\S]、[\w\W]、[\d\D]都可以表示任意字符

// 匹配任意字符
console.log(/./.test('\r'));//false
console.log(/[\s\S]/.test('\r'));//true

量词

//表示邮政编码6位数字
/[0-9][0-9][0-9][0-9][0-9][0-9]/;
//等价于
/\d\d\d\d\d\d/;

这个示例利用字符组表示了匹配邮政编码6位数字,这看起来很冗余。如果要排配100个数字,岂不是要复制100次?辛运的是,正则表达式提供了量词,用来设定某个模式出现的次数

{n}       匹配n次
{n,m}     匹配至少n次,最多m次
{n,}      匹配至少n次
?         相当于{0,1}
*         相当于{0,}
+         相当于{1,}
// 表示邮政编码6位数字
/\d{6}/;

量词广泛应用于解析HTML代码

console.log(/<[^<>]+>/.test('<img data-src="default.jpg">'));//true
console.log(/<[^<>]+>/.test('<>'));//false

贪婪模式

默认情况下,量词都是贪婪模式(greedy quantifier),即匹配到下一个字符不满足匹配规则为止

//exec()方法以数组的形式返回匹配结果
/a+/.exec('aaabbbccc'); // ['aaa']

懒惰模式

懒惰模式(lazy quantifier)和贪婪模式相对应,在量词后加问号'?'表示,表示尽可能少的匹配,一旦条件满足就再不往下匹配

/a+?/.exec('aaabbbccc'); // ['a']
{n}?       匹配n次
{n,m}?     匹配至少n次,最多m次
{n,}?      匹配至少n次
??         相当于{0,1}
*?         相当于{0,}
+?         相当于{1,}

懒惰模式很有用,比如匹配第一个<script></script>之间的代码

var str = '<script>alert(123)</script><script>alert(456)</script>';
var p = /<script>[\s\S]*?<\/script>/;
console.log(p.exec(str));

括号

括号有两个功能,分别是分组和捕获

分组

如果把一个表达式用括号包围起来,则这个表达式被称为子表达式

如果希望字符串'ab'重复出现2次,应该写为(ab){2}。如果写为ab{2},则{2}只限定b

console.log(/(ab){2}/.test('abab'));//true
console.log(/ab{2}/.test('abab'));//false

console.log(/(ab){2}/.test('abb'));//false
console.log(/ab{2}/.test('abb'));//true

应用一: 身份证号码检测

身份证长度有15位和18位两种,如果只匹配长度,可能会想当然地写成\d{15,18},实际上这是错误的,因为它包括15、16、17、18这四种长度。因此,正确的写法应该是\d{15}(\d{3})?

应用二: email地址检测

email地址以@分隔成两段,之前的部分是用户名,之后的部分是主机名

用户名允许出现数字、字母和下划线,长度一般在1-64个字符之间,则正则可表示为/\w{1,64}/

主机名一般表现为a.b.···.c,其中c为主域名,其他为级数不定的子域名,则正则可表示为/([-a-zA-z0-9]{1,63}.)+[-a-zA-Z0-9]{1,63}/

var p =/\w{1,64}@([-a-zA-z0-9]{1,63}\.)+[-a-zA-Z0-9]{1,63}/;
console.log(p.test('q@qq.com'));//true

捕获

通过捕获分组,可以获取每个分组匹配的文本

JavaScript有9个用于存储捕获组的构造函数属性,RegExp.$1、RegExp.$2、RegExp.$3……到RegExp.$9分别用于存储第一、第二……第九个匹配的捕获组。在调用exec()或test()方法时,这些属性会被自动填充

比如,要匹配诸如2018-08-12这样的日期字符串

/(\d{4})-(\d{2})-(\d{2})/.test('2018-08-12');
console.log(RegExp.$1);//'2018'
console.log(RegExp.$2);//'08'
console.log(RegExp.$3);//'12'
console.log(RegExp.$4);//''

exec()方法是专门为捕获组而设计的,返回的数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串

console.log(/(\d{4})-(\d{2})-(\d{2})/.exec('2018-08-12'));

replace()方法就是用于进行数据替换的,该方法接收两个参数,第一个参数为待查找的内容,而第二个参数为替换的内容

console.log('2018-08-12'.replace(/-/g,'.'));// 2018.08.12

replace()方法中也可以引用分组,形式是$num,num是对应分组的编号

console.log('2018-08-12'.replace(/(\d{4})-(\d{2})-(\d{2})/g,'$3-$2-$1')); 
// '12-08-2018'

replace()方法的第二个参数也可以是一个函数,函数参数第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串

'2018-08-12'.replace(/(\d{4})-(\d{2})-(\d{2})/g, function(str, $1, $2, $3) {
  return $3 + '-' + $2 +'-' + $1
})
// "12-08-2018"

反向引用

反向引用(back-reference)允许在正则表达式内部引用之前捕获分组匹配的文本,形式是\num,num表示所引用分组的编号

// 重复字母
console.log(/([a-z])\1/.test('aa'));//true
console.log(/([a-z])\1/.test('ab'));//false

([a-z])这个分组匹配到的是a,所以\1表示的就是a

反向引用可以用于建立前后联系。比如,HTML标签的开始标签和结束标签是对应的

// 开始标签
<([^>]+)>
// 标签内容
[\s\S]*?
// 结束标签
<\/\1>
// 匹配成对的标签
/<([^>]+)>[\s\S]*?<\/\1>/

console.log(/<([^>]+)>[\s\S]*?<\/\1>/.test('<a>123</a>'));//true
console.log(/<([^>]+)>[\s\S]*?<\/\1>/.test('<a>123</b>'));//false

非捕获

非捕获分组(non-capturing group),以(?:)的形式表示,它只用于限定作用范围,而不捕获任何文本

由于非捕获分组不捕获文本,对应地,也就没有捕获组编号。也不可以使用反向引用

// 捕获
console.log(/(abc){2}/.test('abcabc'));//true
console.log(RegExp.$1);//'abc'
// 非捕获
console.log(/(?:abc){2}/.test('abcabc'));//true
console.log(RegExp.$1);//''
/(123)\1/.test('123123');// true
/(?:123)\1/.test('123123');// false

捕获分组和非捕获分组可以在一个正则表达式中同时出现

console.log(/(\d)(\d)(?:\d)(\d)(\d)/.exec('12345'));//["12345", "1", "2", "4", "5"]

竖线

竖线'|'在正则表达式中表示或(OR)关系的选择,以竖线'|'分隔开的多个子表达式也叫选择分支或选择项。在一个选择结构中,选择分支的数目没有限制

选择项的尝试匹配次序是从左到右,直到发现了匹配项,如果某个选择项匹配就忽略右侧其他选择项,如果所有子选择项都不匹配,则整个选择结构匹配失败

console.log(/12|23|34/.exec('1'));//null
console.log(/12|23|34/.exec('12'));//['12']
console.log(/12|23|34/.exec('23'));//['23']
console.log(/12|23|34/.exec('2334'));//['23']

应用一: IP地址检测

ip地址一般由3个点号和4段数字组成,每段数字都在0-255之间

(00)?\d; //0-9、000-009
0?[1-9]\d;//10-99、010-099
1\d{2};//100-199
2[0-4]\d;//200-249
25[0-5];//250-255

//数字(0-255)
/(00)?\d|0?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]/;
// ip地址
var ip = /^(((00)?\d|0?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}\2$/;
console.log(ip.test('1.1.1.1'));//true
console.log(ip.test('1.1.1'));//false
console.log(ip.test('256.1.1.1'));//false

应用二: 时间匹配

时间匹配也需要分段处理

0\d|1[0-2] // 月(01-09|10-12)
0\d|[12]\d|3[01] // 日(01-09|10-29|30、31)
0\d|1\d|2[0-4] // 小时(01-09|10-19|20、21、22、23、24)
0\d|[1-5]\d|60 // 分钟(01-09|10-59|60)
var p = /(0\d|1[0-2])-(0\d|[12]\d|3[01])-(0\d|1\d|2[0-4])-(0\d|[1-5]\d|60)/;
console.log(p.test('08-12-16-25')); // true
console.log(p.test('13-12-16-25')); // false
/0+\d|1[0-2]/.test('34')

应用二:手机号匹配

手机号一般是11位,前3位是号段,后8位一般没有限制。

//前3位
13\d|14[579]|15[0-35-9]|17[0135-8]|18\d
//后8位
\d{8}

//手机号码
var phone = /(13\d|14[579]|15[0-35-9]|17[0135-8]|18\d)\d{8}/;
console.log(phone.test('13312345678'));//true
console.log(phone.test('14312345678'));//false

断言

在正则表达式中,有些结构并不真正匹配文本,而只负责判断在某个位置左/右侧是否符合要求,这种结构被称为断言(assertion),也称为锚点(anchor),常见的断言有3种:单词边界、行开头结尾、环视

单词边界

在文本处理中可能会经常进行单词替换,比如把row替换成line。但是,如果直接替换,不仅所有单词row都被替换成line,也会把字符串内部的row替换成line。要想解决这个问题,必须有办法确定单词row,而不是字符串row

正则表达式提供了专用的单词边界(word boundary),记为\b,它匹配的是'单词边界'位置。与\b对应的还有\B,表示非单词边界

console.log(/\ban\b/.test('an apple'));//true
console.log(/\ban\b/.test('and'));//false

开头结尾

常见的断言还有^和$,它们分别匹配字符串的开始位置和结束位置,所以可以用来判断整个字符串能否由表达式匹配

//匹配第一个单词
console.log(/^\w*/.exec('first word\nsecond word\nthird word'));//['first']
//匹配最后一个单词
console.log(/\w*$/.exec('first word\nsecond word\nthird word'));//['word']

^和$的常用功能是删除字符串首尾多余的空白,类似于字符串String对象的trim()方法

function strTrim(str){
   return str.replace(/^\s+/, '').replace(/\s+$/, '')
}  
console.log(strTrim('      hello world   '));//'hello world'

环视

环视(look-around),可形象地解释为停在原地,四处张望,它本身不匹配任何字符,仅相当于一个匹配条件。环视分为正序环视(先行断言)和逆序环视(后行断言),而JavaScript只支持正序环视,相当于只支持向前看,不支持往回看

而正序环视又分为肯定正序环视和否定正序环视。肯定正序环视的记法是(?=n),表示前面必须是n才匹配;否定正序环视的记法是(?!n),表示前面必须不是n才匹配

// a后面必须是b,才会执行匹配a
console.log(/a(?=b)/.exec('abc'));//['a']
console.log(/a(?=b)/.exec('ac'));//null

// a后面必须不是b,才会执行匹配a
console.log(/a(?!b)/.exec('abc'));//null
console.log(/a(?!b)/.exec('ac'));//['a']

// a后面必须是b,才会执行匹配ab
console.log(/a(?=b)b/.exec('abc'));//['ab']

匹配模式

匹配模式(match mode)指匹配时使用的规则。设置特定的模式,可能会改变对正则表达式的识别

i

默认地,正则表达式是区分大小写的,通过设置标志'i',可以忽略大小写(ignore case)

console.log(/ab/.test('aB'));//false
console.log(/ab/i.test('aB'));//true

m

默认地,正则表达式中的^和$匹配的是整个字符串的起始位置和结束位置,而通过设置标志'm',开启多行模式,它们也能匹配字符串内部某一行文本的起始位置和结束位置

console.log(/world$/.test('hello world\n')); //false
console.log(/world$/m.test('hello world\n')); //true

console.log(/^b/.test('a\nb')); //false
console.log(/^b/m.test('a\nb')); //true

g

默认地,第一次匹配成功后,正则对象就停止向下匹配了。g修饰符表示全局匹配(global),设置'g'标志后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换

console.log('1a,2a,3a'.replace(/a/,'b'));//'1b,2a,3a'
console.log('1a,2a,3a'.replace(/a/g,'b'));//'1b,2b,3b'

优先级

//从上到下,优先级逐渐降低
\                            转义符
() (?!) (?=) []              括号、字符组、环视
* + ? {n} {n,} {n,m}         量词
^ $                          起始结束位置
|                            选择
posted @ 2021-09-29 14:23  wmui  阅读(318)  评论(0编辑  收藏  举报