# 正则表达式是什么

正则表达式是用于匹配字符串中字符组合的模式。在 JavaScript 中,正则表达式也是对象。这些模式被用于 JavaScript 的 RegExp 方法中,例如 test()exec() ,以及字符串的 match()replace()search()split() 方法。

# 创建正则表达式

正则表达式可以通过两种方式在 JavaScript 中创建:

  1. 字面量:使用两个斜杠包围的模式。

    let regex = /ab+c/;
  2. 构造函数:使用 RegExp 对象的构造函数。

    let regex = new RegExp("ab+c");

# 常用的正则表达式语法元素

  1. 锚点

    • ^ 匹配输入字符串的开始位置。
    • $ 匹配输入字符串的结束位置。
  2. 字符类

    • . 匹配除换行符以外的任何单字符。
    • \d 匹配一个数字字符。等价于 [0-9]
    • \D 匹配一个非数字字符。等价于 [^0-9]
    • \w 匹配字母、数字、下划线。等价于 [A-Za-z0-9_]
    • \W 匹配非字母、数字、下划线。
    • \s 匹配任何空白字符,包括空格、制表符、换行符等。
    • \S 匹配任何非空白字符。
    • [abc] 匹配任何一个指定字符(在此例中为 'a'、'b' 或 'c')。
    • [^abc] 匹配任何不在指定集合中的字符。
  3. 量词

    • * 匹配前面的表达式 0 次或更多次。
    • + 匹配前面的表达式 1 次或更多次。
    • ? 匹配前面的表达式 0 次或 1 次。
    • {n} 精确匹配 n 次。
    • {n,} 至少匹配 n 次。
    • {n,m} 最少匹配 n 次且最多 m 次。
  4. 位置和断言

    • \b 匹配一个单词边界。
    • \B 匹配非单词边界。
    • (?=...) 正向前瞻,匹配的字符串后面必须紧跟着 ...
    • (?!...) 负向前瞻,匹配的字符串后面不能紧跟着 ...
  5. 分组和引用

    • (xyz) 匹配并捕获括号内的表达式,也可以通过 \1\2 等引用。
    • (?:xyz) 只匹配但不捕获,不分配组号。
  6. 选择

    • | 匹配两个或多个分支选择的任意一个。

# 综合示例

假设我们需要从文本中找出所有格式为 (XXX) XXX-XXXX 的美国电话号码:

let text = "Contact numbers are (555) 123-4567 and (321) 987-6543.";
let phoneRegex = /\(\d{3}\) \d{3}-\d{4}/g;
let matches = text.match(phoneRegex);
console.log(matches); // 输出: ["(555) 123-4567", "(321) 987-6543"]

在这个示例中, \(\d{3}\) 匹配三位数字加括号,空格是字面意义上的空格, \d{3}\d{4} 分别匹配电话号码中的三位和四位数字。

# 常用场景

# 切分字符串

用正则表达式切分字符串比用固定的字符更灵活,请看正常的切分代码:

"a b   c".split(" "); // ['a', 'b', '', '', 'c']

嗯,无法识别连续的空格,用正则表达式试试:

"a b   c".split(/\s+/); // ['a', 'b', 'c']

无论多少个空格都可以正常分割。加入 , 试试:

"a,b, c  d".split(/[\s\,]+/); // ['a', 'b', 'c', 'd']

再加入 ; 试试:

"a,b;; c  d".split(/[\s\,\;]+/); // ['a', 'b', 'c', 'd']

如果用户输入了一组标签,下次记得用正则表达式来把不规范的输入转化成正确的数组。

# 分组

除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用 () 表示的就是要提取的分组(Group)。比如:

^(\d{3})-(\d{3,8})$ 分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:

var re = /^(\d{3})-(\d{3,8})$/;
re.exec("010-12345"); // ['010-12345', '010', '12345']
re.exec("010 12345"); // null

如果正则表达式中定义了组,就可以在 RegExp 对象上用 exec() 方法提取出子串来。

exec() 方法在匹配成功后,会返回一个 Array ,第一个元素是正则表达式匹配到的整个字符串,后面的字符串表示匹配成功的子串。

exec() 方法在匹配失败时返回 null

提取子串非常有用。来看一个更凶残的例子:

var re =
  /^(0[0-9]|1[0-9]|2[0-3]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$/;
re.exec("19:05:30"); // ['19:05:30', '19', '05', '30']

这个正则表达式可以直接识别合法的时间。但是有些时候,用正则表达式也无法做到完全验证,比如识别日期:

var re = /^(0[1-9]|1[0-2]|[0-9])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$/;

对于 '2-30''4-31' 这样的非法日期,用正则还是识别不了,或者说写出来非常困难,这时就需要程序配合识别了。

# 贪婪匹配

需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的 0

var re = /^(\d+)(0*)$/;
re.exec("102300"); // ['102300', '102300', '']

由于 \d+ 采用贪婪匹配,直接把后面的 0 全部匹配了,结果 0* 只能匹配空字符串了。

我们想要的是,前面的 ^(\d+) 部分匹配 1023,后面的 (0*) 匹配 00。

必须让 \d+ 采用非贪婪匹配(也就是尽可能少匹配),才能把后面的 0 匹配出来,加个 ? 就可以让 \d+ 采用非贪婪匹配:

var re = /^(\d+?)(0*)$/;
re.exec("102300"); // ['102300', '1023', '00']

# 全局搜索

JavaScript 的正则表达式还有几个特殊的标志,最常用的是 g ,表示全局匹配:

var r1 = /test/g;
// 等价于:
var r2 = new RegExp("test", "g");

全局匹配可以多次执行 exec() 方法来搜索一个匹配的字符串。当我们指定 g 标志后,每次运行 exec() ,正则表达式本身会更新 lastIndex 属性,表示上次匹配到的最后索引:

var s = "JavaScript, VBScript, JScript and ECMAScript";
var re = /[a-zA-Z]+Script/g;
// 使用全局匹配:
re.exec(s); // ['JavaScript']
re.lastIndex; // 10
re.exec(s); // ['VBScript']
re.lastIndex; // 20
re.exec(s); // ['JScript']
re.lastIndex; // 29
re.exec(s); // ['ECMAScript']
re.lastIndex; // 44
re.exec(s); //null,直到结束仍没有匹配到

全局匹配类似搜索,因此不能使用 /^...$/ ,那样只会 最多匹配一次

正则表达式还可以指定 i 标志,表示忽略大小写; m 标志,表示执行多行匹配。