正则 RegExp 前端使用手册

2,319 阅读8分钟

开篇

RegExp(regular expression)正则,常常被用作处理 字符串 规则。使用场景包含两个角度:

  1. 校验,验证字符串是否符合某一规则,常见需求如:校验手机号格式 等;
  2. 匹配/捕获,匹配字符串中符合规则的内容,常见需求如:匹配模板表达式 {{}} 进行变量替换 等。

下面,我们从基础到实际应用,来学习和掌握前端正则的使用。

  1. 正则语法,
  2. 利用正则实现校验,
  3. 利用正则实现捕获,
  4. 工作上常见的正则使用场景。

一、正则语法

1、创建正则表达式

1.1、构造函数方式

正则拥有自己的构造函数,它接收 正则表达式 作为参数,返回一个正则实例,从而具备一些 RegExp 原型上的方法,下面验证输入内容是存在数字:

const reg = new RegExp('\\d+');
reg.test(123); // true
reg.test('abc'); // false

1.2、字面量方式

通过两个斜杠 / / 的方式创建正则表达式,也具备 RegExp.prototype 上的方法,如:test、exec 等。

const reg = /\d+/;
reg.test(123); // true
reg.test('abc'); // false

1.3、两者区别

  • 构造函数方式,使用 \ 时需要多加一个 \ 进行转译,否则会被当作普通字符使用;
  • 如果编写正则表达式时,需要加入 变量 的使用,则只有使用 构造函数方式 定义正则表达式,字面量 方式不支持。

PS:或许你在实际开发中会遇到这样的困惑:假设将构造函数方式的正则定义在一个变量上,如:const reg = '\\d+'; 尽管你在控制台 console.log 打印这个变量,得到 \d+,这仅是调用 console.log 输出时自动将转移字符 \ 解析移除,它并不影响赋值给 new RegExp 正常使用。

2、正则表达式的组成部分

上面我们了解了正则表达式的创建方式,在构造函数的参数、字面量两个斜杠之间的内容,就是编写正则表达式的地方

正则表达式可以由两部分组成:元字符修饰符

2.1、元字符

是编写正则表达式的核心组成部分,用来定义字符串校验和匹配的规则。元字符可以划分为三类:

  • 1)普通元字符:(完全字面量规则,含义代表本身)
正则定义为 /cegz/ 匹配的字符串就是 "cegz"
  • 2)特殊元字符:(单个或多个组合在一起,来表示特殊含义的规则)
1. \ 转译字符,可以将普通字符转为有特殊含义的字符,也可将特殊含义字符转为普通字符
    比如转移特殊字符 . 为普通字符:/2\.5/.test('2.5'); // true
2. ^ 指定开头规则使用的元字符,设置它后,将要求字符串的开头要和这里的元字符规则相匹配
    console.log(/^\d/.test('2023abcd')); // true
3. $ 指定结尾规则使用的元字符,
    console.log(/\d$/.test('abcd2023')); // true
    当 ^$ 都加上,表示:字符串只能是和规则一样的内容,如验证 11 位手机号:/^1\d{10}$/
4. . 代表出了 `\n` 以外的任意字符,

5. \n 代表换行符
6. \d 一个集合,代表 0-9 之间的数字
7. \D 非 0-9 之间数的字,比如是字母(`大写都会与小写规则相反`8. \w 数字、字母、下划线(_) 中的任意一个字符
9. \W 即 非 \w
10. \s 代表一个空白字符(包含 空格、制表符、换行符)
11. \S 即 非 \s
12. \t 代表制表符(tab 键)
13. \b 匹配单词边界,确保匹配的是一个完整的单词

13. | 或的意思,如 x|y 表示 x 或者 y 其中一个
14. [] 其中的意思,如 [xyz] 表示 x、y、z 其中一个
15. [^] 取反的意思,如 [^xy] 表示除 x、y 以外的其他字符
16. [-] 指定范围,如 [a-z] 表示 a ~ z 范围之间的字符

17. () 代表分组,就是划分块,先整合这一块规则,它还有一个作用是`分组匹配`18. (?:) 只匹配,不捕获(具体含义后面 匹配 那里介绍)
  • 3)量词元字符:(设置 普通/特殊 元字符出现的次数)
1. * 代表出现 0 ~ 多次
2. + 代表出现 1 ~ 多次
3. ? 代表出现 0 次或 14. {n} 用括号包裹,代表出现 n 次(n 是一个随意指定的数字)
5. {n,} 代表出现 n ~ 多次
6. {n,m} 代表出现 n ~ m 次

2.2、修饰符

修饰符有三个,用于指定正则的匹配策略:i、m、g

  • i(ignoreCase) 忽略单词大小写匹配;
  • m(multiline) 忽略换行符,进行多行匹配;
  • g(global) 全局匹配,匹配到所有满足条件的结果。

假设我们有一段字符串:

const str = `Google test google test google`;

在没有修饰符的情况下,正则只进行完全匹配,并返回匹配到的第一个信息:

console.log(str.match(/google/)); // ['google', index: 12]

如果加上 i 修饰符,正则会忽略大小写,匹配到 Google

console.log(str.match(/google/i)); // ['Google', index: 0]

修饰符也可以同时使用多个,下面匹配时忽略大小写,并匹配所有与正则有关的结果:

console.log(str.match(/google/gi)); // [ 'Google', 'google', 'google' ]

如果是使用 new RegExp 构造函数方式,修饰符可以作为第二参数去传入。

str.match(new RegExp('google', 'gi')) // ['Google', 'google', 'google']

在了解了 元字符修饰符 后,我们基于它们来编写正则表达式,来实现 校验 和 匹配。

二、利用正则实现校验

RegExp.test 通常用来进行正则校验,当实例化正则表达式后,正则就会拥有该方法,接收字符串作为参数进行匹配,若校验成功返回 true,否则返回 false。

我们编写正则验证手机号格式是否正确:以 1 开头,11 位数字

const reg = /^1\d{10}$/;
const iphone = '18933112266';
console.log(reg.test(iphone)); // true

假如我们要实现输入框只允许输入数字,设置 type=number 后会发现还允许输入字母 e 和 小数点 .,我们可以通过正则校验严格控制输入规则:

<Input value={value} onChange={event => {
  const value = event.target.value;
  // 只允许输入数字,并且允许输入为空
  if (/^$|^\d+$/.test(value)) {
    setValue(value);
  }
}} />

三、利用正则实现捕获

实现对字符串内容进行捕获有两种方式:正则表达式 exec 捕获字符串 match 捕获,下面我们来看一看两者区别。

1、RegExp.exec

正则表达式提供了一个 exec 方法用于实现捕获,接收字符串作为参数,匹配结果为:

  • 如果没有匹配到,结果为 null
  • 如果匹配到会返回一个数组,数组元素第一项为匹配的结果,第二项为匹配结果的起始位置(从 0 开始)。
let str = "abcd2023efgh0301";
let reg = /\d+/;
console.log(reg.exec(str));

// 输出:
[ '2023', index: 4, input: 'abcd2023efgh0301', groups: undefined ]

需要注意的是:执行 exec 只会匹配到一个符合规则的结果(惰性匹配),默认只捕获第一个。

之所以惰性匹配是因为正则表达式有一个特殊属性 lastIndex,标记正则要匹配的起始索引。默认这个值不会随调用 exec 的次数调整:

console.log(reg.lastIndex);
console.log(reg.exec(str));
console.log(reg.lastIndex);

// 输出:
0
[ '2023', index: 4, input: 'abcd2023efgh0301', groups: undefined ]
0

如何解决正则惰性?直接修改 lastIndex 不可行,通过为正则表达式添加修饰符 g 实现全局匹配,在每次执行 exec 后会自动更新 lastIndex:

console.log(reg.lastIndex);
console.log(reg.exec(str));
console.log(reg.lastIndex);

0
[ '2023', index: 4, input: 'abcd2023efgh0301', groups: undefined ]
8

不过这样每次需要手动调用 exec 才能匹配到下一个,我们编写 execAll 来帮助我们实现全部匹配:

~function () {
  function execAll(str = '') {
    if (!this.global) return this.exec(str); // 没有加 g,只捕获一次
    let ary = [], res = null;
    while (res = this.exec(str)) {
      ary.push(res[0]);
    }
    return ary.length === 0 ? null : ary;
  }
  RegExp.prototype.execAll = execAll;
}();

console.log(reg.execAll(str));

// 输出:
[ '2023', '0301' ]

通常,我们若想实现全部捕获,会采用另一种捕获方式来代替 execAll 的实现:字符串 match 捕获

2、String.match

字符串原型上提供了 match 方法,接收正则表达式作为参数,它的匹配结果和 exec 很相似:

  • 如果没有匹配到,结果为 null
  • 如果匹配到会返回一个数组,数组元素第一项为匹配的结果,第二项为匹配结果的起始位置(从 0 开始)。

注意,如果正则表达式中还使用了 () 捕获,则会将 () 的捕获结果放入第二项,匹配结果的起始位置 将会往后排列。

let str = "abcd2023efgh0301";
let reg = /\d+/;
console.log(str.match(reg));

// 输出:
[ '2023', index: 4, input: 'abcd2023efgh0301', groups: undefined ]

如果你想实现全部匹配,只需在正则表达式上添加修饰符 g 即可:

let str = "abcd2023efgh0301";
let reg = /\d+/g;
console.log(str.match(reg));

// 输出:
[ '2023', '0301' ]

到这里你会发现,上面两种方式都是匹配结果,下面还有一种方式可以实现匹配,并且还可将匹配结果替换为新的值。(这在业务中经常会用到)

3、String.replace

字符串 replace 方法通常用作字符串替换,第一参数代表匹配的规则,可以是 字符串字面量正则表达式;第二参数代表要替换的值,可以是 替换字符串值处理函数,处理函数需要返回 替换字符串值。

  1. 如果第二参数为字符串,最简单的使用如下:
const str = "abcd 2023 abcd";
console.log(str.replace('2023', 'abcd')); // abcd abcd abcd
  1. 如果第二参数为函数,在匹配到结果时会被执行,需要返回一个字符串作为替换后的新值:
const str = "abcd 2023 abcd";
console.log(str.replace('2023', () => {
  return 'abcd';
})); // abcd abcd abcd

既然是函数,自然会具备很高的灵活性,可以在函数体内编写复杂逻辑。重要的是,函数的参数可以为我们提供匹配信息。

如:第一参数 $1 代表匹配的结果,第二参数 $2 代表匹配结果的起始索引,第三参数 $3 代表原字符串。在某些场景下可以基于这些参数做一些特殊处理。

console.log(str.replace('2023', (...args) => {
  console.log(args); // [ '2023', 5, 'abcd 2023 abcd' ]
  return 'abcd';
}));

对于第一参数如果是 字符串字面量,只会匹配和替换第一个结果,即执行一次替换一次:

let str = "abcd 2023 abcd";
console.log(str.replace("abcd", "2023").replace("abcd", '2023'));

如果需要对字符串进行全局替换,第一参数可选用 正则表达式 来实现:

const str = "abcd 2023 abcd";
console.log(str.replace(/abcd/g, (...args) => {
  console.log(args);
  return '2023';
}));

// 输出结果如下:
[ 'abcd', 0, 'abcd 2023 abcd' ]
[ 'abcd', 10, 'abcd 2023 abcd' ]
2023 2023 2023

通常正则表达式会包含一些 分组 的逻辑,如我们要对时间的分隔符进行替换,我们通过正则可以这样实现:

const time = '2023-03-01';
const reg = /^(\d{4})-(\d{1,2})-(\d{1,2})$/;
console.log(time.replace(reg, '$1年$2月$3日')); // 2023年03月01日

正则通过 () 元字符实现分组,这时 RegExc 原型上就为我们提供了每一个分组匹配结果,通过 $1-xx 来记录。

对于第二参数为函数,同样也可以适应分组匹配:

const time = '2023-03-01';
const reg = /^(\d{4})-(\d{1,2})-(\d{1,2})$/;
console.log(time.replace(reg, (target, $1, $2, $3) => {
  console.log(target, $1, $2, $3); // 2023-03-01 2023 03 01
  return `${$1}${$2}${$3}日`;
})); // 2023年03月01日

在实际业务中,replace 可以做的事情有很多,如解析模板语法 {{var}} 实现变量替换,下面我们来看看几个常见的应用场景。

四、正则应用场景

1、校验输入内容

1.1、验证邮箱格式

用户可以在页面 Input 输入对方邮箱,在发送前我们校验输入内容是否符合一个标准邮箱格式。

// \w+ -> 数字、字母、下划线(_) 中的任意一个字符,出现 1 到 多次
// ([-+.]\w+)* -> 从 [-+.] 中选其中一个拼接 \w+,出现 0 到 多次
// @ --> 包含 @ 符号
// \w+ -> 同上
// ([-.]\w+)* --> 同上,少了一个 + 字符
// \. --> 包含 . 符号
// \w+ --> 同上
// ([-.]\w+)* --> 同上

// 字面量形式
const emailRegex = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
// 构造函数形式
// const emailRegexString = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$";
// const emailRegex = new RegExp(emailRegexString);

// 测试
console.log(emailRegex.test("user@example.com")); // true
console.log(emailRegex.test("user123@gmail.com")); // true
console.log(emailRegex.test("user.name@example.co.uk")); // true
console.log(emailRegex.test("invalid_email")); // false
console.log(emailRegex.test("user@localhost")); // false

1.2、验证链接格式

用户可以在页面 Input 输入一件商品的访问链接,在保存时我们校验输入内容是否符合一个标准的 链接 格式。

function isValidURL(url) {
  const regex = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/;
  return regex.test(url);
}

// 测试
console.log(isValidURL("http://example.com")); // true
console.log(isValidURL("https://example.com")); // true
console.log(isValidURL("example.com")); // true
console.log(isValidURL("www.example.com")); // true
console.log(isValidURL("http://example")); // false
console.log(isValidURL("example")); // false

2、驼峰命名转为短横线(-)命名

使用过 React JSX 的同学都知道:为元素设置 style 属性时需要采用 小驼峰 命名,如:fontSize: 16px,所以我们定义样式时会采用这种方式。

我们回顾一下原生 HTML 为 DOM 添加样式的方式:<div style="font-size: 12px;"></div> 采用短横线 - 命名方式。

假设现在有一个需求:将 React JSX 节点及样式生成原生 HTML 模板文件提供给后台使用。

那我们知道:小驼峰 fontSize 肯定不能正常运行在 HTML 内的,需要转换为 font-size,正则可以帮助我们快速实现:

'fontSize'.replace(/[A-Z]/g, val => `-${val.toLowerCase()}`);

3、模板变量替换

有时候我们需要对一段字符串模板进行解析,将模板内的插槽替换为实际变量,replace 可以很方便的实现。假设我们有模板数据:

let str = "{{user_name}} - {{user_sex}}";

借助正则分组捕获,我们可以很轻松的实现变量捕获及替换:

str = str.replace(/{{(\w+)}}/g, (content, $1) => {
  console.log(content, $1);
  return $1 === 'user_name' ? '明里人' : '男'
});
console.log(str);

打印输出如下:

{{user_name}} user_name
{{user_sex}} user_sex
明里人 - 男

4、手机号中间 4 位使用星号代替

在一些业务场景下,考虑用户隐私安全,会将手机号的中间四位采用 * 星号来代替,借助正则捕获和 replace 可以轻松实现:

'18712345678'.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');

// 得到:
'187****5678'

5、判断一个文件是否为 JS 模块

如果你在团队内是做工程化相关的工作,可能会涉及读取文件内容的场景,针对 JS 模块文件做一些处理,这就需要先判断文件是否为 JS 模块。

function isJsModuleFile(filename) {
  // 定义正则表达式 方式一:显示声明出每一种文件后缀
  // const regex = /\.(js|ts|jsx|tsx|vue)$/;

  // 定义正则表达式 方式二:利用元字符简化实现
  const regex = /\.[jt]sx?|m[jt]s|vue$/;

  return regex.test(filename); // 返回结果
}

// 示例调用
console.log(isJsModuleFile("example.txt")); // false
console.log(isJsModuleFile("example.js")); // true
console.log(isJsModuleFile("example.ts")); // true
console.log(isJsModuleFile("example.jsx")); // true
console.log(isJsModuleFile("example.tsx")); // true
console.log(isJsModuleFile("example.mjs")); // true
console.log(isJsModuleFile("example.mts")); // true
console.log(isJsModuleFile("example.vue")); // true

6、匹配以 params 参数形式的路由

假设有一个 写作 页面,代码中路由定义为:/prowrite/:id,在浏览器使用:https://host/prowrite/xxxxx, 后面的 id 表示写作 id 存在不确定性。

现在我们需要在代码中判断,当前程序是否运行在 /prowrite/xxxxx 页面,可通过正则来实现:

// 正则含义介绍:
// 1)以 /prowrite/ 开头;
// 2)\w+ 表示匹配写作 id,任意多个字符;
// 3)\/? 表示路由可以使用 / 结尾,也可以不使用。
/^\/prowrite\/\w+\/?$/.test(location.pathname)

当然,如果你想拿到 params.id,可使用 match 来捕获。

// 这里为 \w+ 增加了 () 用于实现捕获结果
const res = location.pathname.match(/^\/prowrite\/(\w+)\/?$/)
  • 若路由与正则匹配不正确,则 res = null
  • 若路由与正则匹配正确,则 res = ['/prowrite/abced', 'abced', index: 0, input: '/prowrite/abced', groups: undefined]。其中数组第一项为 整个正则 的匹配结果,数组第二项为我们为 \w+ 所设置的 () 捕获结果,即我们想要的 params.id

持续更新中...