正则 RegExp 前端使用手册

lxf2023-04-05 21:50:02

开篇

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

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

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

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

一、正则语法

1. 创建正则表达式

  1. 构造函数方式

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

const reg = new RegExp('\\d+');
reg.test(123); // true
reg.test('abc'); // false
  1. 字面量方式:

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

const reg = /\d+/;
reg.test(123); // true
reg.test('abc'); // false
  1. 二者区别:
  • 构造函数方式,使用 \ 时需要多加一个 \ 进行转译,否则会被当作普通字符使用;

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. | 或的意思,如 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. 修饰符:

修饰符有三个,用于指定正则的匹配策略: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' ]

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

二、利用正则实现校验

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. 驼峰命名转为短横线(-)命名

使用过 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()}`);

2. 模板变量替换

有时候我们需要对一段字符串模板进行解析,将模板内的插槽替换为实际变量,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
明里人 - 男

更新中...

最后

感谢阅读,如有不足之处,欢迎指出。