一、JS中如何创建正则表达式

1. 字面量:

1
const rex = /pattern/;

2. 构造函数:

1
const rex = new RegExp("pattern");

这两种方法的一大区别是对象的构造函数允许传递带引号的表达式,通过这种方式就可以动态创建正则表达式。通过这两种方法创建出来的 Regex 对象都具有相同的方法和属性:

1
2
3
4
5
let RegExp1 = /a|b/
let RegExp2 = new RegExp('a|b')

console.log(RegExp1) // 输出结果:/a|b/
console.log(RegExp2) // 输出结果:/a|b/

二、JSRegExp实例

1. 实例方法

RegExp 实例置了test()exec()这两个方法来校验正则表达式。

(1)test()

test() 用于检测一个字符串是否匹配某个模式,匹配返回true,否则返回false。

1
2
3
4
5
6
const regex1 = /a/ig;
const regex2 = /hello/ig;
const str = "Action speak louder than words";

console.log(regex1.test(str)); // true
console.log(regex2.test(str)); // false

(2)exec()

exec() 用于检索字符串中的正则表达式的匹配。 该函数返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。

1
2
3
4
5
6
const regex1 = /a/ig;
const regex2 = /hello/ig;
const str = "Action speak louder than words";

console.log(regex1.exec(str)); // ['A', index: 0, input: 'Action speak louder than words', groups: undefined]
console.log(regex2.exec(str)); // null

在当在全局正则表达式中使用 exec 且有标志位时,每隔一次就会返回null,如图:

没有标志位时则正常:

这是怎么回事呢?MDN 的解释如下:

在设置了 global 或 sticky 标志位的情况下(如 /foo/g or /foo/y),JavaScript RegExp 对象是有状态的。他们会将上次成功匹配后的位置记录在 lastIndex 属性中。使用此特性,exec() 可用来对单个字符串中的多次匹配结果进行逐条的遍历(包括捕获到的匹配),而相比之下, String.prototype.match() 只会返回匹配到的结果。

为了解决这个问题,我们可以在运行每个exec命令之前将lastIndex赋值为 0:

2. 实例属性

RegExp 实例还内置了一些属性,这些属性可以获知一个正则表达式的各方面的信息,但是用处不大。

三、模式匹配

关于正则表达式最复杂的地方就是如何编写正则规则了,下面就来看如何编写正则表达式。

1. 修饰符

  • g:表示全局模式,即运用于所有字符串;
  • i:表示不区分大小写,即匹配时忽略字符串的大小写;
  • m:表示多行模式,强制 $ 和 ^ 分别匹配每个换行符。

使用构造函数的方式来构造正则表达式时如何使用修饰符:

1
2
let regExp = new RegExp('[2b|^2b]', 'gi')
console.log(regExp) // 输出结果:/[2b|^2b]/gi

2. 字符集合[] 和 分组()

/[bcf]at/ig

当然,字符集也可以用来匹配数字:

那么如果想要多个字母同时被限定怎么办呢?正则表达式中用小括号 () 来做分组,也就是括号中的内容作为一个整体。

1
2
// 匹配字符串中包含 0 到多个 ab 开头:
^(ab)*

3. 字符范围(区间)

如果我们想要在字符串中匹配所有以 at 结尾的单词,最直接的方式是使用字符集,并在其中提供所有的字母。对于这种在一个范围中的字符,就可以直接定义字符范围,用-表示。它用来匹配指定范围内的任意字符。这里就可以使用/[a-z]at/ig

可以看到,正则表达式按照我们的预期匹配了。

常见的使用范围的方式如下:

  • 部分范围:[a-f],匹配 a 到 f 的任意字符;
  • 小写范围:[a-z],匹配 a 到 z 的任意字符;
  • 大写范围:[A-Z],匹配 A 到 Z 的任意字符;
  • 数字范围:[0-9],匹配 0 到 9 的任意字符;
  • 符号范围:[#$%&@]
  • 混合范围:[a-zA-Z0-9],匹配所有数字、大小写字母中的任意字符。

4. 数量字符(又称重复限定符)

如果想要匹配三个字母的单词,根据上面我们学到的字符范围,可以这样来写:

1
[a-z][a-z][a-z]

这里我们匹配的三个字母的单词,那如果想要匹配10个、20个字母的单词呢?难道要一个个来写范围吗?有一种更好的方法就是使用花括号 {} 来表示,来看例子:

  • :代表前面的字符最多只可以出现一次(0,1);
  • *: 代表字符可以不出现,也可以出现一次或多次(0,1,多次);
  • +: 代表前面的字符必须至少出现一次(1,多次);
  • {n}:重复n此;
  • {n,}:重复n此或更多次
  • {3,15}: 表示3~15个字符的长度(重复3到15次);

5. 元字符

  • \d:相当于[0-9],匹配任意数字;
  • \D:相当于[^0-9]
  • \w:相当于[0-9a-zA-Z],匹配任意数字、大小写字母和下划线;(或汉字?)
  • \W:相当于:[^0-9a-zA-Z]
  • \s:相当于[\t\v\n\r\f],匹配任意空白符,包括空格,水平制表符\t,垂直制表符\v,换行符\n,回车符\r,换页符\f;
  • \S:相当于[^\t\v\n\r\f],表示非空白符。

6. 特殊字符

  • .:匹配除了换行符之外的任何单个字符;
  • \:将下一个字符标记为特殊字符、或原义字符、或向后引用、或八进制转义符;
  • |:逻辑或操作符;
  • [^]:取非,匹配未包含的任意字符。

7. 位置匹配

如果我们想匹配字符串中以某些字符结尾的单词,以某些字符开头的单词该如何实现呢?正则表达式中提供了方法通过位置来匹配字符:

  • \b:匹配一个单词边界,也就是指单词和空格间的位置;
  • \B:匹配非单词边界;
  • ^:匹配开头,在多行匹配中匹配行开头(^ex 匹配以ex开头的);
  • $:匹配结尾,在多行匹配中匹配行结尾(abc$ 匹配字母abc并以abc结尾);
  • (?=p):匹配 p 前面的位置;
  • (?!=p):匹配不是 p 前面的位置。

8. 捕获组

(1)正则表达式中的“捕获组”是指使用括号 () 将子模式括起来(捕获组是建立在()的基础上),以便于在搜索时同时匹配多个项或将匹配的内容单独提取出来。组可以根据需要进行嵌套,形成复杂的匹配模式。

例如:使用捕获组,可以直接在正则表达式 /(Testing|tests) 123/ig 中匹配到 “Testing 123” 和 “Tests 123”,而不需要重复写 “123” 的匹配项。

(2) 正则表达式中的两种常见组类型:

  • (...):捕获组,用于匹配任意三个字符。
  • (?:...):非捕获组,也是用于匹配任意三个字符,但不进行捕获。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. 可以使用以下 JavaScript 将文本替换为Testing 234和tests 234:
const regex = /(Testing|tests) 123/ig;

let str = `
Testing 123
Tests 123
`;

str = str.replace(regex, '$1 234');
console.log(str);
// Testing 234
// Tests 234

// 2. 被括号包围的子模式称为“捕获组”,捕获组可以从匹配的字符串中提取出指定的部分并单独使用。这里我们使用 $1 来引用第一个捕获组 (Testing|tests)。也可以匹配多个组,比如同时匹配 (Testing|tests) 和 (123)。
const regex = /(Testing|tests) (123)/ig;

let str = `
Testing 123
Tests 123
`;

str = str.replace(regex, '$1 #$2');
console.log(str);
// Testing #123
// Tests #123"
1
2
3
4
5
6
7
8
9
10
11
12
// (?: ) 的语法用于创建非捕获组
const regex = /(?:Testing|tests) (123)/ig;

let str = `
Testing 123
Tests 123
`;

str = str.replace(regex, '$1');
console.log(str);
// 123
// 123

(3)命名捕获组

虽然捕获组非常有用,但是当有很多捕获组时很容易让人困惑。$3$5 这些名字并不是一目了然的。为了解决这个问题,正则表达式引入了“命名捕获组”的概念。例如,(?<name>...) 就是一个命名捕获组,名为 “name”,用于匹配任意三个字符。

1
2
3
4
const regex = /Testing (?<num>\d{3})/
let str = "Testing 123";
str = str.replace(regex, "Hello $<num>")
console.log(str); // "Hello 123"

(4)命名反向引用

根据捕获组的命名规则,反向引用可分为:

  • 数字编号组反向引用:\k\number
  • 命名编号组反向引用:\k 或者\'name'

有时候需要在查询字符串中引用一个命名捕获组,这就是“反向引用”的用武之地。

假设有一个字符串,其中包含多个单词,我们想要找到所有出现两次或以上的单词。可以使用具名捕获组和命名反向引用来实现。

1
2
3
4
const regex = /\b(?<word>\w+)\b(?=.*?\b\k<word>\b)/g;
const str = 'I like to eat pizza, but I do not like to eat sushi.';
const result = str.match(regex);
console.log(result); // like

这里使用了具名捕获组 (?<word>\w+)来匹配单词,并将其命名为 “word”。然后使用命名反向引用 (?=.*?\b\k<word>\b) 来查找文本中是否存在具有相同内容的单词。

9. 前瞻组和后顾组(零宽断言)

前瞻组(Lookahead)和后顾组(Lookbehind)是正则表达式中非常有用的工具,它们用于在匹配过程中进行条件约束,而不会实际匹配这些约束的内容。它们使得我们可以更精确地指定匹配模式。

前瞻组:

  • 正向前瞻((?=...)):又叫正向先行断言。用于查找在某个位置后面存在的内容。例如,A(?=B) 可以匹配 “A”,但只有在后面跟着 “B” 时才进行匹配。
  • 负向前瞻((?!...)):用于查找在某个位置后面不存在的内容。例如,A(?!B) 可以匹配 “A”,但只有在后面不跟着 “B” 时才进行匹配。

后顾组:

  • 正向后顾((?<=...)):又叫正向后行断言。用于查找在某个位置前面存在的内容。例如,(?<=A)B 可以匹配 “B”,但只有在其前面跟着 “A” 时才进行匹配。
  • 负向后顾((?<!...)):用于查找在某个位置前面不存在的内容。例如,(?<!A)B 可以匹配 “B”,但只有在其前面不跟着 “A” 时才进行匹配。

10. 贪婪和非贪婪

11. 反义

四、字符串方法

在 JavaScript 内置了 6 个常用的方法是支持正则表达式的,下面来分别看看这些方法。

1
2
3
4
5
6
7
8
9
// search()  方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回子串的起始位置。如果没有找到任何匹配的子串,则返回 -1。
const regex1 = /a/ig;
const regex2 = /p/ig;
const regex3 = /m/ig;
const str = "Action speak louder than words";

console.log(str.search(regex1)); // 输出结果:0
console.log(str.search(regex2)); // 输出结果:8
console.log(str.search(regex3)); // 输出结果:-1

2. match()

1
2
3
4
5
6
7
8
9
// 和exec()返回值一样。但是记住exec的弊端
const regex1 = /a/ig;
const regex2 = /a/i;
const regex3 = /m/ig;
const str = "Action speak louder than words";

console.log(str.match(regex1)); // 输出结果:['A', 'a', 'a']
console.log(str.match(regex2)); // 输出结果:['A', index: 0, input: 'Action speak louder than words', groups: undefined]
console.log(str.match(regex3)); // 输出结果:null

3. matchAll()

1
2
3
4
5
6
7
8
// matchAll() 方法返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。因为返回的是遍历器,所以通常使用for...of循环取出。
for (const match of 'abcabc'.matchAll(/a/g)) {
console.log(match)
}
//["a", index: 0, input: "abcabc", groups: undefined]
//["a", index: 3, input: "abcabc", groups: undefined]

// 需要注意,该方法的第一个参数是一个正则表达式对象,如果传的参数不是一个正则表达式对象,则会隐式地使用 new RegExp(obj) 将其转换为一个 RegExp 。另外,RegExp必须是设置了全局模式g的形式,否则会抛出异常 TypeError

4. replace()

1
2
3
4
5
// replace() 用于在字符串中用一些字符串替换另一些字符串,或替换一个与正则表达式匹配的子串。
const regex = /A/g;
const str = "Action speak louder than words";

console.log(str.replace(regex, 'a')); // 输出结果:action speak louder than words

5. replaceAll()

1
2
3
4
5
6
7
// 和replace相比,该函数会替换所有匹配到的子字符串。
const regex = /a/g;
const str = "Action speak louder than words";

console.log(str.replaceAll(regex, 'A')); // 输出结果:Action speAk louder thAn words

// 需要注意,当使用一个 regex 时,您必须设置全局("g")标志, 否则,它将引发 TypeError:"必须使用全局 RegExp 调用 replaceAll"。

6. split()

1
2
3
4
5
// split() 方法用于把一个字符串分割成字符串数组。其第一个参数是一个字符串或正则表达式,从该参数指定的地方分割字符串。
const regex = / /gi;
const str = "Action speak louder than words";

console.log(str.split(regex)); // 输出结果:['Action', 'speak', 'louder', 'than', 'words']

五、实用工具

  1. Regex101

  2. RegExr

  3. RegexPal

  4. Regex-Vis

  5. vscode插件:Regex previewer

六、总结

  1. JSRegExp有两个实例方法:test() 和 exec(),exec中有一个lastIndex的问题。

  2. String中有6个方法是支持正则表达式的:search()、match()、matchAll()、replace()、replaceAll()、split()。

  3. 位置匹配:(?=p)、(?!=p)

  4. 捕获组:()、(?:)、\k

  5. 零宽断言:(?=)、(?!)、(?<=)、(?>!)

  6. 贪婪:*?、+?、??、{}?

  7. 反义:\大写字母、[^]