Hitsuki9's Blog.

JS 正则表达式

字数统计: 2.6k阅读时长: 10 min
2019/09/25 Share

字符匹配

正则表达式是匹配模式,要么匹配字符,要么匹配位置。

模糊匹配

横向模糊匹配

横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况。

其实现的方式是使用量词,譬如 {m,n},表示连续出现最少 m 次,最多 n 次。

正则 /ab{2,5}c/ 表示匹配这样一个字符串:第一个字符是 "a",接下来是 2 到 5 个字符 "b",最后是字符 "c"

1
2
3
4
let regex = /ab{2,5}c/g;
let string = 'abc abbc abbbc abbbbc abbbbbc abbbbbbc';
console.log(string.match(regex));
// => ["abbc", "abbbc", "abbbbc", "abbbbbc"]

match() 方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。

g 是正则的一个修饰符,表示全局匹配,即在目标字符串中按顺序找到满足匹配模式的所有子串,强调的是“所有”,而不只是“第一个”。


纵向模糊匹配

纵向模糊指的是,一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种可能。

其实现的方式是使用字符组,譬如 [abc],表示该字符是可以字符 "a""b""c" 中的任何一个。

/a[123]b/ 可以匹配如下三种字符串: "a1b""a2b""a3b"

1
2
3
4
let regex = /a[123]b/g;
let string = 'a0b a1b a2b a3b a4b';
console.log(string.match(regex));
// => ["a1b", "a2b", "a3b"]

 

字符组

范围表示法

[123456abcdefGHIJKLM],可以写成 [1-6a-fG-M],即用连字符 - 来省略和简写。

要匹配 "a""-""z" 这三者中任意一个字符,可以写成 [-az][az-][a\-z],即要么放在开头,要么放在结尾,要么转义。


排除字符组

[^abc],表示的是一个除了 "a""b""c"之外的任意一个字符。字符组的第一位放 ^(脱字符),表示求反的概念。

当然,也可以使用范围表示法。


常见的简写形式

字符组 含义
\d 表示 [0-9]
记忆方式:其英文是 digit(数字)。
\D 表示 [^0-9]
\w 表示 [0-9a-zA-Z_]
记忆方式:其英文是 word,也称单词字符。
\W 表示 [^0-9a-zA-Z_]
\s 表示 [ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。
记忆方式:其英文是 space。
\S 表示 [^ \t\v\n\r\f]
. 表示 [^\n\r\u2028\u2029]。通配符,表示几乎任意字符,换行符、回车符、行分隔符和段分隔符除外。

如果要匹配任意字符可以使用 [\d\D][\w\W][\s\S][^] 中任何的一个。

 

量词

简写形式

量词 含义
{m,} 表示至少出现 m 次。
{m} 等价于 {m,m},表示出现 m 次。
? 等价于 {0,1},表示出现或者不出现。
+ 等价于 {1,},表示出现至少一次。
* 等价于 {0,},表示出现任意次,有可能不出现。

贪婪匹配与惰性匹配

1
2
3
4
let regex = /\d{2,5}/g;
let string = '123 1234 12345 123456';
console.log(string.match(regex));
// => ["123", "1234", "12345", "12345"]

正则 /\d{2,5}/ 表示数字连续出现 2 到 5 次,会匹配 2 位、3 位、4 位、5 位连续数字,其是贪婪的,会尽可能匹配多的位数。

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

惰性匹配,就是尽可能少的匹配位数,正则 /\d{2,5}?/ 表示虽然出现 2 到 5 次都行,但当 2 个就够的时候,就不再往下尝试了。

通过在量词后面加个 ? 就能实现惰性匹配。

 

多选分支

多选分支可以支持多个子模式任选其一。

具体形式如下:/p1|p2|p3/,其中 p1p2p3 是子模式,用 |(管道符)分隔,表示匹配其中任何一个模式即可。

分支结构也是惰性的,即当前面的匹配上了,后面的就不再尝试了。

1
2
3
4
let regex = /good|goodbye/g;
let string = 'goodbye';
console.log(string.match(regex));
// => ["good"]

 

匹配字符,无非就是字符组、量词和分支结构的组合使用。

 

位置匹配

位置

位置(锚)是相邻字符之间的位置。

位置可以替换成字符。

在 ES5 中,共有 6 个锚:^$\b\B(?=p)(?!p)

^ 和 $

^(脱字符)匹配开头,在多行匹配中匹配行开头。

$(美元符号)匹配结尾,在多行匹配中匹配行结尾。

多行匹配模式时(有修饰符 m),会匹配每一行。

1
2
3
4
5
6
7
let result = 'I\nlove\njavascript'.replace(/^|$/gm, '#');
console.log(result);
/*
#I#
#love#
#javascript#
*/

\b 和 \B

\b 是单词边界,具体就是 \w\W 之间的位置,也包括 \w^ 之间的位置和 \w$ 之间的位置。

\B 就是 \b 的反面的意思,非单词边界,具体说来就是 \w\w\W\W^\W\W$ 之间的位置。


(?=p) 和 (?!p)

(?=p),其中 p 是一个子模式,整体表示匹配模式 p 的字符的前面的位置。

1
2
3
let result = 'hello'.replace(/(?=l)/g, '#');
console.log(result);
// => "he#l#lo"

(?!p) 就是 (?=p) 的反面意思。

二者的学名分别是 positive lookahead 和 negative lookahead,中文翻译分别是正向先行断言和负向先行断言。

ES5 之后的版本,会支持 (?<=p) (positive lookbehind) 和 (?<!p)(negative lookbehind)。

 

位置的特性

位置可以理解成字符之间的空字符 "",且在两个字符之间可以有多个。

1
2
3
let result = /(?=he)^^he(?=\w)llo$\b\b$/.test('hello');
console.log(result);
// => true

 

括号

括号提供了分组,便于我们引用它。

引用某个分组会有两种情形:在 JavaScript 里引用它和在正则表达式里引用它。

分组和分支结构

强调括号内的正则是一个整体,即提供子表达式。

分组

正则 /a+/ 匹配连续出现的 "a",而要匹配连续出现的 "ab" 时,需要使用 /(ab)+/,其中括号提供分组功能,使量词 + 作用于 "ab" 这个整体。


分支结构

1
2
3
4
5
let regex = /^I love (JavaScript|Regular Expression)$/;
console.log(regex.test('I love JavaScript'));
console.log(regex.test('I love Regular Expression'));
// => true
// => true

如果去掉括号,则匹配的字符串是 "I love JavaScript""Regular Expression"

 

分组引用

括号的一个重要作用,有了它,就可以进行数据提取以及更强大的替换操作。

提取数据

1
2
3
4
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let string = '2017-06-12';
console.log(string.match(regex));
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]

match 返回一个数组,第一个元素是整体匹配的结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。另外,正则表达式若有修饰符 gmatch 返回只包含整体匹配结果而不包含分组匹配内容的数组。

另外也可以使用正则实例对象的 exec 方法。

1
2
3
4
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let string = '2017-06-12';
console.log(regex.exec(string));
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]

使用 exec(), 不论正则是否带 g 修饰符,只要没有分组,其返回只包含一个匹配结果的类数组。如果带 g 修饰符,则可以通过反复调用 exec() 来获取下一个包含匹配结果的类数组,并且匹配下标 index 会更新至下一个匹配结果的开头在字符串中的位置,当调用时没有匹配结果时返回 null。如果不带 g 修饰符,则反复调用都将返回包含第一个匹配结果的类数组。

也可以使用构造函数的全局属性 $1$9 来获取。

1
2
3
4
5
6
7
8
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let string = '2017-06-12';
regex.test(string); // 正则操作即可,例如
//regex.exec(string);
//string.match(regex);
console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"

替换

yyyy-mm-dd 格式替换成 mm/dd/yyyy 格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let string = '2017-06-12';
let result = string.replace(regex, '$2/$3/$1');
console.log(result);
// 等价于
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let string = '2017-06-12';
let result = string.replace(regex, function() {
return RegExp.$2 + '/' + RegExp.$3 + '/' + RegExp.$1;
});
console.log(result);
// 等价于
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let string = '2017-06-12';
let result = string.replace(regex, function(match, year, month, day) {
return month + '/' + day + '/' + year;
});
console.log(result);
// => "06/12/2017"

 

反向引用

除了可以使用相应的 API 来引用分组,也可以在正则本身里引用分组,但只能引用之前出现的分组,即反向引用。

1
2
3
4
5
6
7
8
9
let regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
let string1 = '2017-06-12';
let string2 = '2017/06/12';
let string3 = '2017.06.12';
let string4 = '2016-06/12';
console.log(regex.test(string1)); // true
console.log(regex.test(string2)); // true
console.log(regex.test(string3)); // true
console.log(regex.test(string4)); // false

\1 表示引用之前的那个分组 (-|\/|\.),不管它匹配到什么(比如 -),\1 都匹配与分组匹配到的同样的具体的某个字符。

以此类推,\2\3 分别指代第二个和第三个分组。

括号嵌套

以左括号(开括号)为准。

1
2
3
4
5
6
7
let regex = /^((\d)(\d(\d)))\1\2\3\4$/;
let string = '1231231233';
console.log(regex.test(string)); // true
console.log(RegExp.$1); // 123
console.log(RegExp.$2); // 1
console.log(RegExp.$3); // 23
console.log(RegExp.$4); // 3

\10

\10 表示的是第 10 个分组而非 \10

如果真要匹配 \10 的话,请使用 (?:\1)0 或者 \1(?:0)。((?:) 表示非捕获分组,匹配的值不会保存起来)


引用不存在的分组

当在正则里引用了不存在的分组时,正则不会报错,只是去匹配反向引用的字符本身。


分组后有量词

分组后若有量词,分组最终捕获到的数据是最后一次的匹配,对于反向引用也是如此。

1
2
3
4
5
let regex = /(\d)+ \1/;
console.log(regex.test('12345 1'));
// => false
console.log(regex.test('12345 5'));
// => true

 

非捕获括号

如果只想使用括号最原始的功能,并不想引用它,可以使用非捕获括号 (?:p)(?:p1|p2|p3)

CATALOG
  1. 1. 字符匹配
    1. 1.1. 模糊匹配
      1. 1.1.1. 横向模糊匹配
      2. 1.1.2. 纵向模糊匹配
    2. 1.2. 字符组
      1. 1.2.1. 范围表示法
      2. 1.2.2. 排除字符组
      3. 1.2.3. 常见的简写形式
    3. 1.3. 量词
      1. 1.3.1. 简写形式
      2. 1.3.2. 贪婪匹配与惰性匹配
    4. 1.4. 多选分支
  2. 2. 位置匹配
    1. 2.1. 位置
      1. 2.1.1. ^ 和 $
      2. 2.1.2. \b 和 \B
      3. 2.1.3. (?=p) 和 (?!p)
    2. 2.2. 位置的特性
  3. 3. 括号
    1. 3.1. 分组和分支结构
      1. 3.1.1. 分组
      2. 3.1.2. 分支结构
    2. 3.2. 分组引用
      1. 3.2.1. 提取数据
      2. 3.2.2. 替换
    3. 3.3. 反向引用
      1. 3.3.1. 括号嵌套
      2. 3.3.2. \10
      3. 3.3.3. 引用不存在的分组
      4. 3.3.4. 分组后有量词
    4. 3.4. 非捕获括号