前言
正则表达式可以用来描述文本模式,使用正则表达式可以准确地匹配目标文本,减少匹配目标文本所需代码量,也可用于约束文本内容。Javascript实现了正则表达式,本文中将介绍如何在Javascript中定义和使用正则表达式。
文章目录
一、什么正则表达式
正则表达式是一种描述文本模式的对象。我们可以使用正则表达式来描述文本模式,从而可以对文本进行快速便捷的搜索替换匹配操作。
正则表达式的创建方式有两种:
- 使用RegExp()构造函数创建,例如
let mode = new RegExp('\\d{5}', 'g')
- 使用正则表达式字面量创建,例如
let mode = /\d{5}/g
但无论是上面哪种方式,都需要定义正则表达式模式。
正则表达式模式由一系列字符组成。多数字符,包括所有字母数字字符,都只用来描述直接匹配的字符,例如/java/
就匹配任何包含字串java的字符串。正则表达式中还有一些字符并不直接匹配字符本身,而是具有特殊含义,例如$
匹配字符串末尾,/s$/
匹配末尾的s
。正则表达式也支持标志,比如i
标识符表示匹配不区分大小写,标识符在RegExp构造函数的第二个参数或者正则表达式字面量的第二个斜杠后定义。
接下来本文将介绍正则表达式的语法。
二、 字符字面量
所有字母字符和数字在正则表达式中都匹配自身的字面量。JavaScript正则表达式语法通过以\开头的转义字符也支持一些非字母字符,例如a
会匹配字符串中的a
,a1
会匹配字符串中的a1
,\n
匹配字符串中换行字符的字面量。
Javascript中的字符匹配如下表所示:
字符 | 匹配目标 |
---|---|
字母数字字符 | 自身 |
\0 | NUL字符 |
\t | 制表符(空格的效果) |
\n | 换行符 |
\v | 垂直制表符 |
\f | 进纸符 |
\r | 回车符 |
值得注意的是,有一些英文标点符号在正则表达式中具有特殊含义:^ $ . * + ? = ! : | \ / ( ) [ ] { }
。如果在正则表达式中想表达的是这些字符本身的字面量的话需要在前面加上\
来进行转意,从而表达自身字面量。
三、 字符类
只使用字符字面量的话仅能匹配字符本身或者其所代表的特殊含义。当想要匹配几个字符中的任意一个、某个范围内的字符或排除某些字符的话,就可以使用字符类。
[]
,将字面量字符放入方括号中可以组成字符类。正则表达式会匹配方括号中包含的任意字符。例如/[abc]/
这个正则表达式会匹配a、b、c
三个字母中的任意一个。- 定义排除性的字符类,通过将
^
作为方括号中的第一个字符,就会匹配除方括号中包含的字符之外的字符。例如[^abc]
这个正则表达式会匹配除abc之外的任意一个字符。 - 字符类也使用连字符
-
来表示字符范围。例如[a-z]
这个正则表达式匹配小写字母。
字符类的用法如下:
字符 | 匹配目标 |
---|---|
[…] | 方括号中的任意一个字符 |
[^…] | 不在方括号中的任意一个字符 |
. | 除换行或其他unicode行终止符之外的任意字符。如果RegExp使用s标志,则句点匹配任意字符,包括行终止符 |
\w | 任意Ascii单词字符。等价于[a-zA-Z0-9_] |
\W | 任意非Ascii单词字符。等价于[^a-zA-Z0-9_] |
\s | 任意Unicode空白字符 |
\S | 任意非Unicode空白字符 |
\d | 任意ascii数字字符。等价于[0-9] |
\D | 任意非ASCII数组字符。等价于[^0-9] |
[\b] | 退格字符字面值(特例) |
四、 重复
假如我们想匹配连续的字符或者字符组合,就可以使用重复来表达。例如/\d\d/
用来描述两位数字,/\d\d\d\d/
用来描述四位数字,再多位的话描述起来就会很繁琐,而重复就可以解决这种问题,/\d{4}/
匹配四位数字。
重复的用法如下表所示:
字符 | 含义 |
---|---|
{n,m} | 匹配前项至少n次不超过m次 |
{n,} | 匹配前项n或更多次 |
{n} | 匹配前项恰好n次 |
? | 匹配前项0或1次。等价于{0,1} |
+ | 匹配前项一或多次。等价于{1,} |
* | 匹配前项零或多次。等价于{0,} |
例如:
let r = /\d{2,4}/; // 匹配两位到四位数字
let r1 = /\w{3}\d?/; // 匹配三个字母后边可选一位数字
let r2 = /\s+java\s+/; // java前后至少一位的空格
let r3 = /[^(]*/; // 匹配0到多个非(字符
注意:
使用*?
时要小心,因为他们会匹配前面的字符或模式的零个实例,也就是说他们可以匹配不存在。例如,/a*/
会匹配'bbbb'
,因为这个字符串包含零个字母a。
五、非贪婪重复
重复会尽可能多地匹配,同时也会允许正则表达式剩余的部分继续匹配,这种重复是贪婪的。例如,/a*/
在匹配字符串aaa
时,会匹配全部三个字母。
非贪婪重复使用时在重复后面加个?
即可,这样就可以使重复变为非贪婪的,例如/a*/?
只会匹配第一个a
。只要在任意重复后面加上?
,都会变为非贪婪重复,例如?? +? {1,5}?
。
注意:
使用非贪婪重复并不总是能得到想要的结果,比如说字符串aaab
,正则表达式/a+?b/
可能想要匹配的是ab
,但非贪婪重复会匹配第一个a
,最终匹配的结果是aaab
六、任选、分组和引用
竖线字符|
用于分隔任选模式,例如,/ab|cd|ef/
匹配字符串ab
或字符串cd
或字符串ef
,而/\d{3}|[a-z]{4}/
匹配三个数字或四个小写字母。任选模式在匹配的时候,会从左到右依次适配任选模式。如果左边的任选模式匹配,则会忽略右边的任选模式,即使右边的模式可以得到更好的匹配, 例如模式/a|ab/
应用到字符串ab
只会匹配到字母a
。
圆括号()
在正则表达式中有几种不同的作用:
- 把独立的模式分组为子表达式,从而让
|、*、+、?
当做一个整体。例如/java(Script)?/
匹配java
后跟可选的Script
。而/(ab|cd)?|ef/
匹配ef
或者一个或多个ab
或cd
。 - 在完整的模式中定义子模式。当正则表达式成功匹配一个目标字符串后,可以从目标字符串中提取出与圆括号包含的子模式对应的部分。例如,要查找一个或多个小写字母后跟一个或多个数字,可以使用模式
/[a-z]+\d+/
。但假设我们只关心每个匹配项中的数字部分,如果把匹配数字的模式放到一对圆括号中/[a-z/+(\d+)/
,那么就可以从整个模式的匹配项中提取出相应的数字。 - 与圆括号分组的子表达式相关的一个用途是在同一个正则表达式中回引子表达式。回引前面的子表达式要使用
\
字符加上数字。这里的数字指的是圆括号分组的子表达式在整个正则表达式中的位置。例如,\1
回引第一个子表达式\2
回引第二个子表达式,其位置是按照左括号来计算的。例如,/([Jj]ava([Ss]cript)?)\sis\s(fun\w*)/
表达式中,([Ss]cript)
要用\2
来引用。注意的是,对正则表达式前面子表达式的引用不会引用该子表达式的模式,而是引用该表达式的匹配到的文本。例如:/(['"])[^'"]*\1/
前面匹配的是'
后边就会匹配'
,这样可以匹配成对的''
或""
。 - 如果不想要圆括号分组的子表达式生成数字引用,那么可以不使用
(
和)
分组,而是开头用?:
,(?:[Ss]cript)
仅仅是一个分组。
七、指定匹配位置
正则表达式的很多组件匹配字符串中的一个字符。例如,\s
匹配一个空白符。还有一些正则表达式组件匹配字符间的位置而非实际的字符,例如\b
匹配ASCII词边界,\w
匹配ASCII单词字符与非单词字符的边界,或者ASCII单词字符与字符串开头或末尾的边界,像\b
这样的组件并不表示匹配的字符串中用到的任何字符,他们表示匹配可以发生的合法位置。这些组件也被称作正则表达式锚点。因为他们把模式锚定到被搜索字符串中特定的位置。例如,/^javascript$/
匹配独占一行的javascript字符串。
也可以使用任意正则表达式作为锚定条件。如果在(?=和)
之间包含一个表达式,这些字符构成向前查找断言,所以就意味着其中的字符必须存在,但并不实际匹配。比如,要匹配常用的编程语言的名字,但后边必须跟着一个冒号,可以使用/[Jj]ava([Ss]cript)?(?=\:)
/,这个模式匹配'Javascript: The Definitive Guide'
中的'Javascript'
,但不匹配'Java in a Nutshell'
中的Java
,因为他后面没有:
。
如果把前面的断言改为以(?!
开头,那就变成了否定式向前查找断言,表示必须不存在断言中指定的字符。例如/Java(?!Script)([A-Z]\w*)/
匹配Java
跟一个大写字母及任意数量的ASCII单词字符,但Java
后面必须不能是Script
。因此他匹配JavaBeans
,不匹配Javanese
,匹配JavaScrip
,不匹配JavaScript
或JavaScripter
。
指定匹配位置的用法如下表所示:
字符 | 含义 |
---|---|
^ | 匹配字符串开头,或在使用m标志时,匹配一行的开头 |
$ | 匹配字符串末尾,或在使用m标志时,匹配一行的末尾 |
\b | 匹配单词边界 |
\B | 匹配非单词边界 |
(?=p) | 肯定式向前查找断言。要求后面的字符匹配模式p,但匹配结果不包含与之匹配的字符 |
(?!p) | 否定式向前查找断言。要求后面的字符不匹配模式p |
八、标志
每个正则表达式都会带一个或多个标志,用于修改其行为。标志在正则表达式字面量中放在第二个斜杠后面,在RegExp()构造函数放在第二个参数。
g
: 表示正则表达式是全局性的。使用这个标志意味着想要找到字符串中所包含的所有匹配项。i
: 表示匹配模式应该不区分大小写m
: 表示模式应该以多行模式进行。^ $ 既匹配字符串的开头和末尾,也匹配字符串中任何一行的开头和末尾s
: 用在要搜索的文本包含换行符的时候u
: u标志代表Unicodey
: y标志表示正则表达式是有粘性的,应该在字符串开头匹配或在紧跟前一个匹配的第一个字符串出匹配
九、模式匹配的字符串方法
1. search
接收一个正则表达式参数,返回第一个匹配项起点字符的位置,如果没有找到匹配项,则返回-1。如果参数不是正则表达式,则会交给RegExp()函数,把他转为正则表达式。search方法不支持全局搜索。标志g会被忽略。
let word = /java/;
let text = 'javascript';
console.log(text.search(word)); // 0
let text2 = 'c++';
console.log(text2.search(word)); // -1
2. replace
执行搜索替换操作。接收一个正则表达式作为第一个参数,接收一个替换字符作为第二个参数。
- 如果正则表达式带
g
标志,replace方法会用替换字符串替换所有的匹配项。否则,只会替换第一个匹配项。 - 假如第一个参数不是正则表达式,不会用RegExp函数将其转换为正则表达式,而是按字面量搜索。
- 正则表达式中括号分组的子表达式从左往右编号的,而且正则表达式能够记住每个子表达式匹配的文本。如果替换字符串中出现了$符号后跟一个数字,replace会将两个字符串替换为与指定子表达式匹配的文本。
let word = /java/;
let text = 'javascript java';
let res = text.replace(word, 'no')
console.log(res); // noscript java
let word2 = /java/g;
let text2 = 'javascript java';
let res2 = text.replace(word2, 'no');
console.log(res2); // noscript no
let text = 'javascript java';
let res = text.replace('java', 'no')
console.log(res); // noscript java
let word = /"([^"]*)"/;
let text = 'he said "top"';
// $1是第一个分组
let res = text.replace(word, '<<$1>>');
console.log(res); // he said <<top>>
3. match
它只有一个正则表达式参数,如果参数不是正则表达式,会传给RegExp()构造函数,返回一个数组,其中包含匹配结果。如果没有匹配项,则返回null。如果正则表达式有g
标志,这个方法返回的数组会包含在字符串中找到的所有匹配项。如果正则表达式没有g
标志,match不会执行全局搜索,只会查找第一个匹配项,数组剩余项是捕获组匹配的子字符串。在非全局搜索时,match仍然返回数组,但数组元素不同。
let text = 'visit my blog at http://www.baidu.com/~david';
let url = /(\w+):\/\/([\w.]+)\/(\S*)/;
let match = text.match(url);
if (match !== null) {
let [fullurl, protocol, host, path] = match;
console.log(fullurl, protocol, host, post); // http://www.baidu.com/~david http www.baidu.com ~david
}
非全局搜索情况下,match返回的数组除了可以通过数值索引元素,也有一些对象属性。input属性引用调用match的字符串,index属性是匹配项在字符串中的起始位置,如果正则表达式包含命名捕获组,则返回的数组也有一个groups属性,其值是一个对象,对象的属性就是命名捕获组的名字,属性值就是匹配的文本。
let text = 'visit my blog at http://www.baidu.com/~david';
let url = /(?<protocol>\w+):\/\/(?<host>[\w.]+)\/(?<path>\S*)/;
let match = text.match(url);
if (match !== null) {
console.log(match.index);
console.log(match.input);
console.log(match.groups);
}
// 17
// visit my blog at http://www.baidu.com/~david
// [Object: null prototype] {
// protocol: 'http',
// host: 'www.baidu.com',
// path: '~david'
// }
4. matchAll
matchAll方法是ES2020中定义的,matchAll()接收一个带g标志的正则表达式。并不想match一样返回所有匹配项的数组,而是返回一个迭代器,每次迭代都产生一个与使用match时传入非全局RegExp得到的匹配对象相同的对象。正因如此,matchAll()成为循环遍历字符串中所有匹配项最简单和通用的方法。
let text = 'this is a word';
let words = /\b[a-zA-Z]+\b/g;
let result = text.matchAll(words);
for (let word of result) {
console.log(word);
}
// [ 'this', index: 0, input: 'this is a word', groups: undefined ]
// [ 'is', index: 5, input: 'this is a word', groups: undefined ]
// [ 'a', index: 8, input: 'this is a word', groups: undefined ]
// [ 'word', index: 10, input: 'this is a word', groups: undefined ]
5. split
以传入的参数作为分隔符,将调用他的字符串拆分为子字符串保存到一个数组中。
"123,456,789".split(',') // ["123","456"."789"]
"1, 2, 3,\n4, 5".split(/\s*,\s*/) // ["1","2","3","4","5"]
十、RegExp匹配方法
1. test
test方法接收一个字符串参数,如果字符串与模式匹配则返回true,如果没有则找到匹配项则返回false。
let match = /java/;
let caseInstance = new RegExp(match, 'i');
console.log(caseInstance.test('i like c plus')); // false
console.log(caseInstance.test('i like javascript')); // true
2. exec
exec方法接受一个字符串参数,并从这个字符串寻找匹配。如果没有找到匹配项,则返回null,如果找到了匹配项则返回数组,跟字符串的match方法在非全局搜索时返回的数组一样。
let match = /java/;
let caseInstance = new RegExp(match, 'i');
console.log(caseInstance.exec('i like c plus')); // null
console.log(caseInstance.exec('i like javascript')); // [ 'java', index: 7, input: 'i like javascript', groups: undefined ]
总结
本文总结了正则表达式的语法,告诉大家应该如何去定义和使用正则表达式。