前端正则那些事儿🧐

2,750 阅读8分钟

引言

在面试中,正则堪称“经典考题”。掌握正则,一把拿下面试。😎

初识正则

1.什么是正则🤔

💡简单来说,就是处理字符串的一种规则。💡

image.png

📌下面是使用正则去验证某个字符串的例子:

function compile(){
    let reg = /1[0-9]/;    // 0-9表示取值范围  []表示取一个
    console.log(reg.test('12'));
}

compile();//true

2. 编写正则表达式

2.1 创建方式

字面量创建方式

📌字面量创建方式是使用两个斜杠 / 将正则表达式包裹起来,内部包含用于描述规则的元字符

let reg1 = /aa/; 
// 匹配包含 "aa" 的字符串
console.log(reg.test('baab'));  //true
console.log(reg1.test('banana')); // false
console.log(reg1.test('apple'));  // false

构造函数模式创建

📌通过 RegExp 构造函数创建正则表达式,将规则以字符串形式传递给它。

let reg2 = new RegExp('aa'); 
// 匹配包含 "aa" 的字符串
console.log(reg.test('baab'));  //true
console.log(reg2.test('banana')); // false
console.log(reg2.test('apple'));  // false

2.2 正则表达式的组成

2.2.1 元字符

正则表达式中的元字符是其核心组成部分,用于定义复杂的匹配规则。元字符分为以下几类:

量词元字符:设置匹配次数
元字符含义示例
*零次或多次/a*/ 匹配 "a", "aaa", ""
+一次或多次/a+/ 匹配 "a", "aaa"
?零次或一次/a?/ 匹配 "a", ""
{n}恰好 n 次/a{2}/ 匹配 "aa"
{n,}至少 n 次/a{2,}/ 匹配 "aa", "aaa"
{n,m}至少 n 次,至多 m 次/a{1,3}/ 匹配 "a", "aa", "aaa"

特殊元字符:具有特殊意义的字符或组合
元字符含义示例
\转义字符,用于转义其他元字符/\*/ 匹配 "*"
.任意单个字符(除换行符 \n/./ 匹配 "a", "1", "$"
^匹配字符串的开头/^a/ 匹配 "apple" 的开头 "a"
$匹配字符串的结尾/a$/ 匹配 "banana" 的结尾 "a"
\n换行符/<br>\n 表示匹配换行符后的内容
\d匹配数字(0-9/\d+/ 匹配 "123"
\D匹配非数字/\D+/ 匹配 "abc"
\w匹配字母、数字、下划线([a-zA-Z0-9_]/\w+/ 匹配 "hello_123"
\s匹配空白字符(空格、制表符、换页符等)/\s+/ 匹配 " "
\t匹配制表符(TAB 键,通常为四个空格)/\t/ 匹配 TAB 键
\b匹配单词边界/\bword\b/ 匹配 "word" 不匹配 "sword"
x|y匹配 x 或 y
[xyz]匹配方括号中任意一个字符/[aeiou]/ 匹配 "a", "e", "i"
[^xy]匹配除括号中字符外的任意字符/[^aeiou]/ 匹配非元音字母
[a-z]匹配指定范围的任意字符/[a-z]/ 匹配 "a", "b", ..., "z"
[^a-z]匹配非指定范围的任意字符/[^a-z]/ 匹配数字或特殊字符
()分组捕获/(ab)+/ 捕获 "abab"
(?:)仅匹配不捕获/(?:ab)+/ 匹配 "abab" 不捕获子组
(?=)正向预查/(?=\d)/ 匹配数字前的位置
(?! )反向预查/(?!a)/ 匹配非 "a" 开头的位置

普通元字符:直接匹配字符本身

📌普通字符如 a, 1, # 等会直接匹配其自身。例如:

let reg = /name/; 
console.log(reg.test("myname")); // true

2.2.2 修饰符

正则表达式的修饰符用于改变其匹配行为。修饰符位于表达式的末尾,例如 /abc/g

修饰符含义示例
i (ignoreCase)忽略大小写匹配/abc/i 匹配 "abc", "ABC"
m (multiline)多行匹配,使 ^$ 匹配每行的开头和结尾/^abc/m 匹配多行中的 "abc"
g (global)全局匹配,匹配所有可能的项/a/g 匹配 "a", "a" in "aaa"
u (Unicode)处理 Unicode 字符(大于 \uFFFF 的字符)/\u{20BB7}/u 匹配特定 Unicode 字符
y (sticky)粘连模式,从 lastIndex 开始进行匹配/abc/y 必须从当前位置匹配 "abc"
s (dotAll). 匹配所有字符,包括换行符 \n 和回车符 \r/./s 匹配 "a", "\n", "\r"

这么多你肯定记不住,不如收藏一下慢慢看😁


2.3 常用元字符详解✍🏻

2.3.1 ^$
  • ^ 开头匹配

📌表示匹配字符串的起始位置,规则写在 ^ 后面。

let reg = /^1[0-9]a/;
console.log(reg.test('12a')); // true
console.log(reg.test('12ab')); // true
console.log(reg.test('a12a')); // false
  • $ 结尾匹配

📌表示匹配字符串的结尾位置,规则写在 $ 前面。

let reg = /1[0-9]a$/;
console.log(reg.test('12a')); // true
console.log(reg.test('12ab')); // false
console.log(reg.test('a12a')); // true
  • 没有^$

📌匹配字符串中包含规则的任意部分即可。

let reg = /1[0-9]a/;
console.log(reg.test('12a')); // true
console.log(reg.test('12ab')); // true
console.log(reg.test('a12a')); // true
  • ^$

📌匹配字符串整体必须符合规则。

let reg = /^1[0-9]a$/;
console.log(reg.test('12a')); // true
console.log(reg.test('12ab')); // false
console.log(reg.test('a12a')); // false

2.3.2 转义字符\
  • 作用
    \ 用于将有特殊意义的字符变为普通字符,或者将普通字符变为有特殊意义的字符。
let reg = /\{.*\}/; // 匹配 "{...}" 的形式
console.log(reg.test("{123}")); // true

2.3.3 x|y
  • 含义:匹配 xy
    注意优先级问题,通常使用小括号 () 来调整规则逻辑。
let reg1 = /^18|29$/; // 匹配以 "18" 开头,或以 "29" 结尾
console.log(reg1.test('18')); // true
console.log(reg1.test('129')); // true

let reg2 = /^(18|29)$/; // 只匹配 "18" 或 "29"
console.log(reg2.test('18')); // true
console.log(reg2.test('129')); // false

2.3.4 中括号 []
  • 含义:匹配括号内任意一个字符

    1. 中括号中的字符「一般」代表其本身意义。
    2. 范围匹配:[a-z] 表示从 az 的任意一个字符。
let reg = /^[18]$/; // 匹配 "1" 或 "8"
console.log(reg.test('1')); // true
console.log(reg.test('8')); // true
console.log(reg.test('18')); // false
let reg = /^[10-29]$/; // 匹配 "1""9" 或者 "0-2"
console.log(reg.test('2')); // true
console.log(reg.test('3')); // false

2.3.5 {n,m}
  • 含义:匹配前面的规则 n 到 m 次
    注意:不加 ^$ 时,会在字符串中找到符合条件的部分进行匹配。
let reg = /\d{2,4}/; // 匹配连续的 2 到 4 位数字
console.log(reg.test('123')); // true
console.log(reg.test('12345')); // true (只匹配前 4 位)

let regStrict = /^\d{2,4}$/; // 整体必须是 2 到 4 位数字
console.log(regStrict.test('12345')); // false
console.log(regStrict.test('1234')); // true

2.3.6 分组作用
  1. 改变优先级

    let reg = /^18|29$/; // 不加括号:匹配以 "18" 开头或以 "29" 结尾
    let regGroup = /^(18|29)$/; // 加括号:只匹配 "18" 或 "29"
    
  2. 分组捕获
    捕获每一个分组的匹配内容。

    let reg = /^(\d{4})-(\d{2})-(\d{2})$/;
    let match = reg.exec('2025-01-08');
    console.log(match[1]); // "2025" (分组 1)
    console.log(match[2]); // "01" (分组 2)
    
  3. 分组引用
    在规则中重复引用之前捕获的分组内容。

    let reg = /^([a-z])\1$/; // 匹配两个相同的小写字母
    console.log(reg.test('aa')); // true
    console.log(reg.test('ab')); // false
    

2.3.7 问号 ? 的五大作用
  1. 量词元字符:出现零次或一次

    let reg = /a?b/; // "a" 可有可无
    console.log(reg.test('b')); // true
    console.log(reg.test('ab')); // true
    
  2. 取消贪婪匹配

    let reg = /a.+?b/; // 匹配最少字符
    console.log("a123b456b".match(reg)); // ["a123b"]
    
  3. 只匹配不捕获 (?:)

    let reg = /(?:ab)c/; // 匹配 "abc" 但不捕获 "ab"
    
  4. 正向预查 (?=)

    let reg = /\d(?=px)/; // 匹配数字后面跟着 "px" 的部分
    console.log(reg.test('100px')); // true
    
  5. 负向预查 (?!)

    let reg = /\d(?!px)/; // 匹配数字后面不是 "px" 的部分
    console.log(reg.test('100px')); // false
    console.log(reg.test('100em')); // true
    

2.4 常用的正则表达式📖

2.4.1 验证手机号

规则:

  • 11 位
  • 1 固定第一位数字
  • [0-9]{10}:表示匹配 10 个连续的0-9之间的数字,即手机号码的后 10 位。
let reg = /^1[0-9]{10}$/;
console.log(reg.test('1387018399'));//false  10位
console.log(reg.test('13870183991'));//true   11位
console.log(reg.test('138701839910'));//false  12位

2.4.2 验证密码

规则:

  • 长度为 6-16 位
  • 必须由数字和字母组成。

正则表达式

let reg = /^(?=.*[a-zA-Z])(?=.*\d)[A-Za-z\d]{6,16}$/;
console.log(reg.test('123456'));       // false: 只有数字
console.log(reg.test('abcdef'));       // false: 只有字母
console.log(reg.test('abc123'));       // true: 字母和数字组合
console.log(reg.test('abc1234567890')); // true: 符合 6-16 位
console.log(reg.test('abc!123'));      // false: 包含特殊字符

解释:

  • (?=.*[a-zA-Z]):至少包含一个字母。
  • (?=.*\d):至少包含一个数字。
  • [A-Za-z\d]{6,16}:6-16 位,由字母和数字组成。

2.4.3 验证真实姓名

规则:

  • 必须是 汉字
  • 长度为 2-6 位

正则表达式

let reg = /^[\u4e00-\u9fa5]{2,6}$/;
console.log(reg.test('王林'));     // true: 符合规则
console.log(reg.test('诸葛孔明')); // true: 符合规则
console.log(reg.test('齐天大圣孙悟空')); // false: 超过 6 位
console.log(reg.test('John'));    // false: 非汉字

2.4.4 验证邮箱

规则:

  • 邮箱用户名由数字、字母、下划线或 . 组成,但 .- 不可连续,也不可作为开头。
  • @ 后面由字母或数字组成。
  • 支持多级域名,如 .com.com.cn 等。

正则表达式

let reg = /^\w+((-\w+)|(.\w+))*@[A-Za-z0-9]+((.[A-Za-z0-9]+)+)$/;
console.log(reg.test('example@gmail.com'));         // true
console.log(reg.test('user.name@example.co.uk'));   // true
console.log(reg.test('user-name@example.com'));     // true
console.log(reg.test('.username@example.com'));     // false: 以 `.` 开头
console.log(reg.test('username@@example.com'));     // false: 多个 `@`

2.4.5 验证身份证号

规则:

  • 总共 18 位,最后一位可以是数字或大写字母 X
  • 前 6 位是地区代码。
  • 接下来的 8 位表示出生日期,格式为 YYYYMMDD
  • 最后 4 位是个人编号,其中倒数第二位表示性别(奇数为男性,偶数为女性)。

正则表达式

let reg = /^([1-9]\d{5})((19|20)\d{2})(0[1-9]|1[0-2])(0[1-9]|[1-2]\d|30|31)\d{3}(\d|X)$/i;
console.log(reg.test('11010519900307211X')); // true: 符合规则
console.log(reg.test('110105199003072119')); // true: 符合规则
console.log(reg.test('110105199013072119')); // false: 月份不合法
console.log(reg.test('110105199002302119')); // false: 日期不合法
console.log(reg.test('11010519900307211x')); // true: 最后一位忽略大小写

解释:

  • ([1-9]\d{5}):前 6 位地区代码。
  • ((19|20)\d{2}):出生年份,支持 1900-2099。
  • (0[1-9]|1[0-2]):出生月份,01-12。
  • (0[1-9]|[1-2]\d|30|31):出生日期,01-31。
  • \d{3}(\d|X):个人编号(3 位数字)和校验码(1 位数字或 X)。

3. 正则捕获

正则表达式不仅可以用于匹配,还可以用于提取和捕获字符串中的特定内容。要实现正则捕获,首先需要确保正则匹配成功(即 test 方法返回 true 后才能进行捕获操作)。

常用的涉及正则捕获的工具:

  1. 正则表达式对象方法:如 exectest
  2. 字符串方法:如 replacematchsplit 等。

3.1 exec 方法(正则原型上的方法)

3.1.1 懒惰性:默认只捕获一个结果
  • exec 返回的结果:

    • 第一项:本次捕获到的完整匹配内容。
    • 其余项:每个分组单独匹配到的内容(若正则中包含分组)。
    • index 属性:匹配内容在字符串中的起始索引位置。
    • input 属性:原始字符串。
let str = 'name2020name2020name2020';
let reg = /\d+/;
console.log(reg.exec(str)); 
// ["2020", index: 4, input: "name2020name2020name2020", groups: undefined]

注意exec 默认只捕获第一个匹配结果。


3.1.2 懒惰性的解决方法

默认情况下,lastIndex(正则的下一次匹配起始位置)不会自动更新,因此 exec 每次从字符串开始捕获,导致只能捕获第一个匹配项。

解决方法:设置全局修饰符 g

let str = 'name2020name2020name2020';
let reg = /\d+/g;

let result;
while ((result = reg.exec(str)) !== null) {
  console.log(result); 
  // 每次捕获一个结果
}
// ["2020", index: 4, input: "name2020name2020name2020", groups: undefined]
// ["2020", index: 12, input: "name2020name2020name2020", groups: undefined]
// ["2020", index: 20, input: "name2020name2020name2020", groups: undefined]

3.1.3 贪婪性

正则默认具有贪婪性:会尽可能多地匹配内容。

let str = 'name2020';
let reg = /\d+/g;

console.log(str.match(reg)); // ["2020"]

3.1.4 解决贪婪性问题

在量词元字符后添加 ?,可将捕获变为非贪婪模式(匹配最短结果)。

let str = 'name2020';
let reg = /\d+?/g;

console.log(str.match(reg)); // ["2", "0", "2", "0"]

3.2 正则分组捕获

分组捕获通过小括号 () 实现。使用 execmatch 方法,可以分别获取整个正则匹配的结果及每个分组的结果。

分组捕获规则
  • 大正则:完整匹配的结果。
  • 小分组:每个分组匹配的结果。
// 身份证号验证
let str = '130222195202303210';
let reg = /^([1-9]\d{5})((19|20)\d{2})(0[1-9]|1[0-2])(0[1-9]|[1-2]\d|30|31)\d{3}(\d|x)$/i;

console.log(reg.exec(str));
// [
//   "130222195202303210", // 完整匹配
//   "130222",             // 地区码
//   "1952",               // 出生年份
//   "19",                 // 年份前两位
//   "02",                 // 月份
//   "30",                 // 日期
//   "0"                   // 校验位
// ]

console.log(str.match(reg));
// 效果类似 exec,返回完整匹配及分组结果。

注意:当正则设置了全局修饰符 g 时,match 方法不会捕获分组的内容,只会返回所有完整匹配项。


3.3 非捕获分组

有时我们需要分组仅用于调整优先级,而不需要捕获该分组的内容。此时可以使用 ?: 来定义非捕获分组。

let reg = /(?:\d{4})-(\d{2})-(\d{2})/;
let str = '2023-01-08';

console.log(reg.exec(str));
// ["2023-01-08", "01", "08"]
// 只有后两个分组内容被捕获

3.4 字符串的 match 方法

match 方法在不设置全局修饰符时,功能类似 exec,捕获完整匹配及分组结果。

let str = '2020-01-01';
let reg = /(\d{4})-(\d{2})-(\d{2})/;

console.log(str.match(reg));
// [
//   "2020-01-01", // 完整匹配
//   "2020",       // 年
//   "01",         // 月
//   "01"          // 日
// ]

设置全局修饰符时,match 返回所有完整匹配结果,但不包含分组。

let str = '2020-01-01 and 2021-12-31';
let reg = /\d{4}-\d{2}-\d{2}/g;

console.log(str.match(reg)); // ["2020-01-01", "2021-12-31"]

3.5 字符串原型上的replace方法

实践:

let template = `我是{{name}},年龄{{age}},性别{{sex}}`;
let person = {
    name:'王林',
    age:12,
    sex:'男'
}

如何让上面的字符串 我是{{name}},年龄{{age}},性别{{sex}}替换成 我是王林,年龄12,性别男呢?

方法一:while循环

function compile(template,data){
     let reg = /\{\{([a-z]+)\}\}/;
     while(reg.test(template)){
        let key = reg.exec(template)[1];
        let value = data[key];
        template = template.replace(reg,value);
    }
    return template;
    }
    
    console.log(compile2(template,person));

image.png 成功!🥳🥳🥳

方法二:递归调用

function compile2(template,data){
    const reg = /\{\{(\w+)\}\}/;
    // console.log(Object.prototype.toString.call(reg))
    if(reg.test(template)){
      const name = reg.exec(template)[1];// 拿到分组的key  ()
      template = template.replace(reg,name in data ? data[name] : "");
      return compile2(template,data)// 递归调用
    }else{
      return template;// 没有匹配到的情况返回结果
    }
}

console.log(compile2(template,person));

成功!🥳🥳🥳 image.png 方法三:g

function compile3(template, data) {
    let reg = /\{\{([a-z]+)\}\}/g;
    return template.replace(reg, (match, key) => {
        return data[key] !== undefined ? data[key] : match; // 如果 key 存在于 data 中,则替换
    });
}

console.log(compile3(template, person)); 

image.png 成功!🥳🥳🥳

结语

看到这里,你一定累了吧🥱,整理一下,为拿下面试做准备吧!🎉🎉🎉

点赞哦~😘

Suggestion.gif