正则表达式

260 阅读3分钟

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

基本语法

字符含义
\转义
匹配位置:
匹配输入的开始
$匹配输入的结束
\b匹配单词的开始或结束(所谓单词的开始和结束,就是\b前后不同时是\w)
x(?=y)正向前瞻,仅匹配后面紧跟着y的x
x(?!y)负向前瞻,仅匹配后面不跟着y的x
匹配字符
\w字母、数字、下划线
\d数字
\s空白字符,包括空格、制表符、换页符和换行符
[xyz]匹配[]中的任意字符
x|y匹配x或y
.默认匹配除换行符之外的任何单个字符
量词
*匹配前一个表达式0次或多次
?匹配前一个表达式0次或1次
正则默认是贪婪匹配,也就是匹配尽量多的字符
在量词后的?表示当前匹配是惰性匹配
+匹配前一个表达式1次或多次
{n}匹配前一个表达式n次
{n,}前一个表达式至少出现n次
flags
g全局匹配(global)
i不区分大小写(ignoreCase)
m多行搜索

基本方法

创建一个正则表达式

var reg = /\w+/; // 字面量
// 等价于
var reg = new RegExp('\\w+'); // 构造函数的字符串参数 

注意:当使用构造函数创造正则对象时,需要常规的字符转义规则(也就是在转义字符前也要加反斜杠 \)。

正则表达式(RegExp)的方法

  • test

    /a/.test('a'); // true
    /a/.test('b'); // false
    
  • exec

    regExp是一个有状态的对象,多次对同一个或者相等的字符串执行同样的方法,可以遍历所有匹配的结果。

    相比test会返回更多内容,如果匹配成功会返回一个数组,并且更新lastIndex属性。

    返回的数组其实是类数组,第一项是匹配的字符串,接着是捕获的分组,还有groups、index、input属性。

    var reg = /(a|d)/g;
    var target = 'abcd';
    var arr;
    while(arr = reg.exec(target)) {
        console.log(arr);
    }
    console.log(arr);
    
    // 长度为2的数组
    // ["a", "a", index: 0, input: "abcd", groups: undefined]
    // ["d", "d", index: 3, input: "abcd", groups: undefined]
    // null
    

String对象的方法

  • split

    var values = '1,2,3,4,5'; //以中文或英文逗号分隔的值
    var arr = values.split(/,|,/); // ['1', '2', '3', '4', '5']
    
  • match

    • 如果正则表达式有flag g,那么match方法会返回所有匹配的结果,但不会返回捕获组。

    • 如果没有,就和exec方法很像,会返回第一个匹配和相关的捕获组,以及index、input、groups属性。如果不匹配,则返回null。

    const str = 'Hello World';
    console.log(str.match(/[A-Z]/)); // ["H", index: 0, input: "Hello World", groups: undefined]
    console.log(str.match(/[A-Z]/g)); // ["H", "W"]
    
  • replace

    console.log('abcd'.replace(/ab/, 'AB')); // ABcd
    
  • search

    返回匹配到位置的索引,匹配失败时返回-1。

    console.log('hello World'.search(/[A-Z]/)); // 6
    

分组与捕获

引用

正则表达式中,括号的作用除了使结构更加清晰,还有一个更重要的功能:分组和捕获,也就是可以提取匹配到的数据或进行替换,而这个功能需要配合相关的api使用,比如:exec、match。

假设我们要匹配一个年月日,可以用这样的正则表达式匹配:

var reg = /\d{4}-\d{2}-\d{2}/;
reg.exec('2021-06-01'); // ["2021-06-01", index: 0, input: "2021-06-01", groups: undefined]

虽然匹配了,但要是我们想分别提取到年、月、日的信息呢?考虑下面的正则表达式:

var reg = /(\d{4})-(\d{2})-(\d{2})/;
reg.exec('2021-06-01'); // ["2021-06-01", "2021", "06", "01", index: 0, input: "2021-06-01", groups: undefined]

可以看到,返回的数组多了几项,从第二项开始,分别对应的是三个括号内匹配到的内容,这就是分组,在exec方法中可以捕获到,通过RegExp.11-9,也可以获取到对应的内容。

console.log(RegExp.$1); // 2021console.log(RegExp.$2); // 06console.log(RegExp.$3); // 01

反向引用

除了上面这种引用,还有一种反向引用,也就是正则本身引用之前出现的分组。

考虑这样的场景:我们要匹配三种格式的日期:yyyy-mm-dd、yyyy/mm/dd、yyyy.mm.dd。或许我们可以这样写正则:

var reg = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;

虽然确实可以匹配上面三种格式的日期,但是这个正则表达式同时也匹配了像‘2021-06/02’这样的日期,要怎么样保持连接符号的一致呢?这就需要反向引用了。

var reg = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;

这里的\1,引用的就是前面出现的第一个分组,通过\1\2\3这样的转义字符,可以捕获到前面的分组,无论前面匹配到什么,反向引用匹配的都是同样的具体某个字符。

如果遇到嵌套的括号,则以左括号为准,标识分组顺序。

非捕获分组

还是上面的例子,我们用括号包裹连接符,创建了分组,但捕获的时候,我们其实并不关心连接符号,而只关心匹配到的年月日,那么我们可以使用非捕获分组(?:)

var reg = /(\d{4})(?:-|\.|\/)(\d{2})(?:-|\.|\/)(\d{2})/;console.log(reg.exec('2021-06-01')); // ["2021-06-01", "2021", "06", "01", index: 0, input: "2021-06-01", groups: undefined]

如此,我们捕获到的分组,就只是年、月、日了。

正则的构建

构建正则之前,我们要考虑几个问题:

  • 是否能使用正则

  • 是否有必要使用正则,能用字符串API解决的简单问题,就没有必要使用正则。

  • 是否有必要构建一个复杂的正则

    对于密码匹配,假设规则有很多,那么构建出来的可能是一个很庞大的正则表达式,但其实也可以使用很多个小正则来做

    function checkPassword(string) {  if (!regex1.test(string)) return false;  if (!regex2.test(string)) return false;  if (!regex3.test(string)) return false;  ...}
    

    正则采用的是回溯的方法来匹配,对于太复杂的正则表达式,可能会影响到性能。

构建正则表达式时,还要注意:

  • 匹配预期的字符串
  • 不匹配非预期的字符串
  • 效率优化:
    • 使用具体型字符组代替通配符
    • 使用非捕获型分组
    • 独立出确定字符
    • 提取分支的公共部分
    • 减少分支的数量

使用场景

表单验证/提取

验证输入的某个值是否符合一个模式,使用match方法,如果匹配失败,返回null,匹配成功,则返回带有捕获分组信息的类数组。

切分

从逗号分隔的字符串中提取数据,兼容中文逗号和英文逗号。

const arr = value.split(/,|,/);

待补充...

一个用于测试正则表达式的网站:regex101.com/

参考资料

JS正则迷你书

NEXT学位课程