正正则则表达式~

180 阅读5分钟

前言

  • 相信大家对正则表达式并不会陌生,它是一种描述了字符串的匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等
  • 虽说不陌生,但我对于正则一直以来的态度都非常暧昧,好像在日常工作中,简单的字符串操作并不需要一定使用正则,而到了一些复杂的场景,那就现查
  • 实际这并非一种很好的心态,随着慢慢的深入,一些算法,复杂字符操作,它会变成卡住我的一个点,那么就让我们一起重温正则表达式
  • 这个文章可以让我们轻松认识正则~结合 demo~快速理解~

使用场景 - 为啥要用正则

  • 在前端工作中,我们一般会有以下场景可以使用正则表达式
    • 查找
    • 替换
    • 验证
    • 分割
  • 说得挺单调的,上一个例子
    • 我们需要把一个字符串里面的数字都找出来,用数组形式展示
const str = 'afdsa234fda231dfsa111';
// [234, 231, 111]

// 不适用正则
function getNumber(str) {
  const arr = [];
  let tmp = '';
  for (let i = 0; i < str.length; i++) {
    const s = str[i];
    if (!isNaN(s)) {
      tmp += s;
    } else if (tmp) {
      arr.push(tmp);
      tmp = ''
    }
  }
  tmp && arr.push(tmp);

  return arr
}

// 使用正则
function getNumber(str) {
  const reg = /\d+/g;

  return str.match(reg);
}
  • 从上面的例子我们可以看出,虽然我们可以不适用正则也可以得到相同的结果,但是代码量和工作量并非同一个等量级,这就是使用正则的好处
  • 我们可以通过正则表达式,在对字符串进行一些操作时,更加简单、优雅

正则表达式创建

正则表达式的内容

正则表达式分为两个部分

  1. 需要匹配的内容
    • 我们可以通过一些正则的语法进行模糊匹配
    • 也可以直接把我们需要匹配的字符写上去进行精确匹配
  2. 修饰符
    • 除了指定匹配的内容外,我们可能需要对匹配内容进行指定额外的匹配策略
    • 例如 g 全局匹配 i 不区分大小写 等

字面量创建

  • 使用 /pattern/flags 进行创建
    • pattern - 匹配内容
    • flags - 匹配策略
const str = 'afdsa234aBc231dfsa111';
// const reg = /\d+/g;
const reg = /abc/i;
console.log(str.match(reg));
  • 字面量的创建方式最大的优点就是简单,不麻烦
  • 除了 \* 的语法之外,他会把里面的内容认定为字符串

构造函数创建

  • 在字面量创建时,正则会自动把内容认定为字符串,那么当我们需要使用变量时是操作不了的,这个时候,我们就需要使用构造函数的方式创建
  • new RegExp(pattern[, flags])
    • pattern{String} - 匹配内容
    • flags{String} - 匹配策略
const str = 'afdsa234aBc231dfsa111';
// const reg = new RegExp('\\d+', 'g');
const abc = '231';
const reg = new RegExp(`${abc}df`, 'g');
console.log(str.match(reg));
  • 值得注意的是,我们在使用构造函数创建时,因为是参数是字符串,所以在使用正则语法时,需要加上转义字符 \
  • 也是因为的参数是字符串,我们可以对他使用变量拼接

正则匹配方法

正则对象底下方法

  1. test - 返回一个 Boolean 值:是否匹配
const str = 'abc123fda123';
const reg = /\d+/g;

const bol = reg.test(str);
console.log(bol); // true
  1. exec- 返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null

    • 返回内容

      [0] - 与正则表达式相匹配的文本

      [groups] - 捕获匹配到的文本

      [index] - 匹配到文本的下标

      [input] - 原文本

    • 它会忽略全局匹配,基于 lastIndex 开始匹配,如需从新匹配,则手动将 lastIndex 设置为 0;

const str = 'abc123fda321';
const reg = /(?<test>\d+)/g;

console.log(reg.exec(str)); // [0: "123", 1: "123", groups: {test: '123'}, index: 3, input: "abc123fda321", length: 2]
console.log(reg.exec(str)); // [0: "321", 1: "321", groups: {test: '321'}, index: 9, input: "abc123fda321", length: 2]
// 这里 1 为 RegExp.$1,以此类推

字符串方法

  1. split - 切割字符串,返回数组
    • 我们一般传一个字符对原字符进行切割,除此之外,他也可以接受正则
const str = 'abc123fda3214';

console.log(str.split(1)); // ['abc', '23fda32', '4']

console.log(str.split(/\d+/)); // ['abc', 'fda', '']
  1. search - 查找;类似于 indexOf
    • 它像是一个可以匹配正则的 indexOf
    • 值得注意的是:他不能指定匹配开始的位置,会忽略全局匹配,同时忽略 lastIndexOf
const str = 'abcdefghiejk';
const reg = /e/g;
console.log(str.search(reg)); // 4
  1. match - 匹配
    • 全局匹配 - 得到一个数组,数组内为所有匹配到的字符
    • 非全局匹配 - 与 exec 一致
const str = 'abc11defghiej222k';
console.log(str.match(/(?<test>\d+)/)); // [0: "11", 1: "11", groups: {test: '11'}, index: 3, input: "abc11defghiej222k", length: 2]
console.log(str.match(/(?<test>\d+)/g)); // ['11', '222']
  1. replace - 替换
    • 返回替换后字符串,并不会修改原本字符串
    • 可用函数方式,入参:
      1. arg - 匹配到的字符
      2. groups - 命名分组的字符
      3. index - 匹配到字符的下标
      4. input - 原字符串
const str = 'abc11defghiej222k';
const reg = /\d/g;
console.log(str.replace(reg, '*')); // abc**defghiej***k

str.replace(reg, (arg, groups, index, input) => {
  console.log(arg, groups, index, input);
  return '*'
});

字符相关元字符

  • 含义:有特殊含义的非字母字符

字符相关

修饰符描述
\w数字、字母、下划线
\W非 (数字、字母、下划线)
\d数字
\D非 (数字)
\s空格
\S非 (空格)
.非 (\n\r\u2028\u2029) 注意:模板字符的换行也是换行

数量相关

修饰符描述
{}匹配出现次数,3 种写法,详情看实例
?{0,1} 或 让 贪婪匹配 -> 惰性匹配,详情看实例
+{1,}
*{0,}
  • {}
const str = 'abceeeffd';
// const reg = /ceeef/g; // 这样写过于麻烦,不可能 100 个就写 100 个 e
let reg = /ce{3}f/g; // 匹配 3 个 e
console.log(reg.test(str));

reg = /ce{1,3}f/g; // 匹配 1-3 个 e,闭区间
console.log(reg.test(str));

reg = /ce{1,}f/g; // 匹配 1-n 个 e
console.log(reg.test(str));
  • ?
const str = '123456789';
let reg = /\d{2,4}/g;
console.log(str.match(reg)); // 贪婪匹配(尽可能多的去匹配) ['1234', '5678']

reg = /\d{2,4}?/g;
console.log(str.match(reg)); // 惰性匹配 ['12', '34', '56', '78']

位置相关

修饰符描述
^开始
$结尾
\b边界符(\W 的都是边界)
\B非(边界符)
  • ^
const str = 'abedef';
const reg = /^\w/g;

console.log(str.replace(reg, '*')); // *bedef
  • $
const str = 'abedef';
const reg = /\w$/g;

console.log(str.replace(reg, '*')); // abede*
  • \b
const str = 'this is a book';
const reg1 = /is/g;
console.log(str.match(reg1)); // ['is', 'is'] -> 一个是 this 的,一个是 is 的

const reg2 = /\bis\b/g; // 左右空格都是边界
console.log(str.match(reg2)); // ['is']
  • \B
const str = 'this is a book';
const reg = /\B\w{2}\b/g;
console.log(str.match(reg)); // ['is', 'ok'] -> this['is'] book['ok']

括号相关

修饰符描述
()分组
[]字符集合
|
  • ()

    • 分组
    const str = 'abababfdsafds';
    const reg1 = /ababab/g; // 太麻烦了写 3 次
    console.log(reg1.test(str)); // true
    
    const reg2 = /ab{3}/g; // 上面说到的 匹配出现次数
    console.log(reg2.test(str)); // false -> 因为他匹配出来相当于 /abbb/g
    
    const reg3 = /(ab){3}/g; // 分组的作用
    console.log(reg3.test(str)); // true
    
    • 提取
    const str = '1996-07-11';
    const reg1 = /\d{4}-\d{2}-\d{2}/;
    console.log(str.match(reg1)); // [0: "1996-07-11", groups: undefined, index: 0, input: "1996-07-11", length: 1]
    
    // 通过分组拿到对应值
    const reg2 = /(\d{4})-(\d{2})-(\d{2})/;
    console.log(str.match(reg2)); // [0: "1996-07-11", 1: "1996", 2: "07", 3: "11", groups: undefined, index: 0, input: "1996-07-11", length: 4]
    
    console.log(RegExp.$1); // 1996
    console.log(RegExp.$2); // 07
    console.log(RegExp.$3); // 11
    
    • 替换
    const str = '1996-07-11'; // 11/07/1996
    const reg = /(\d{4})-(\d{2})-(\d{2})/;
    
    console.log(str.replace(reg, '$3/$2/$1')); // 11/07/1996
    const res = str.replace(reg, (arg, year, mouth, date) => `${date}/${mouth}/${year}`);
    console.log(res); // 11/07/1996
    
    • 反向引用
    const className = 'news-container_nav';
    const reg1 = /\w+(-|_)\w+(-|_)\w+/g;
    console.log(reg1.test(className)); // true
    
    // 一般类名的连接符都是统一的,我们需要做这么一个匹配怎么办?
    const reg2 = /\w+(-|_)\w+\1\w+/g; // 通过 \1-n 提取之前的分组
    console.log(reg2.test(className)); // false
    
  • []

    • 一般集合
    const str = 'My name is LiLei'; // 匹配 LiLei,中间的 L 有可能大写也可能小写
    // const reg = /Li(L|l)ei/g;
    const reg = /Li[Ll]ei/g; // 可以把 L、l 放进集合内,里面本身就有 或者 的意思
    console.log(reg.test(str)); // true
    
    • 范围集合
    /[0-9]/;/[a-z]/;/[A-Z]/;
    
    // 不能写类似于 [a-Z],里面的 [x-x] 都是根据 `ASCII` 码顺序排列的
    
    • ^ 取反
    const str = '123456789';
    const reg = /[^0-9]/g; // 中括号内 ^ 意为取反,就是非(0-9)
    
    console.log(reg.test(str)); // false
    
    • 取代字符
    // \d -> [0-9]
    // \w -> [a-zA-Z0-9_]
    

匹配模式

修饰符描述
g全局匹配
i忽略大小写
m多行模式
s.(修饰符) 全匹配
u能匹配 unicode 编码
y粘性模式
  • g
const str = 'abc123daf321fdash';
const reg1 = /\d+/;
console.log(str.match(reg1)); // [0: "123", groups: undefined, index: 3, input: "abc123daf321fdash", length: 1]

const reg2 = /\d+/g;
console.log(str.match(reg2)); // ['123', '321']
  • i
const str = 'abcABc';
const reg1 = /ABC/g;
console.log(reg1.test(str)); // false

const reg2 = /ABC/gi;
console.log(reg2.test(str)); // true
  • m
const str = `abc
efg
hij`;
const reg1 = /^\w/g;
console.log(str.replace(reg1, '*'));
/*
*bc
efg
hij
*/

const reg2 = /^\w/gm;
console.log(str.replace(reg2, '*'));
/*
*bc
*fg
*ij
*/
  • s
const str = `abc
efg`;
const reg1 = /^a.*g$/g;
console.log(reg1.test(str)); // false

const reg2 = /^a.*g$/gs;
console.log(reg2.test(str)); // true
  • u
const str = 'a';
const reg = /\u{61}/gu;
console.log(reg.test(str)); // true
  • y
const str = '12345fdafdsa4324';
const reg1 = /\d/g;
const reg2 = /\d/gy;
console.log(reg1.exec(str)[0], reg2.exec(str)[0]); // 1
console.log(reg1.exec(str)[0], reg2.exec(str)[0]); // 2
console.log(reg1.exec(str)[0], reg2.exec(str)[0]); // 3
console.log(reg1.exec(str)[0], reg2.exec(str)[0]); // 4
console.log(reg1.exec(str)[0], reg2.exec(str)[0]); // 5
console.log(reg1.exec(str)); // [0: 4]
console.log(reg2.exec(str)); // null -> 匹配必须连续的

命名分组和零宽断言

命名分组

  • 在上面 execmatch 方法的时候,我们总会看到一个 groups,其实他就是一个我们自定义命名的一个对象
  • 我们可以通过 ?<xxx> 进行命名分组
const str = '1996-07-11';
const reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
console.log(str.match(reg)); // [0: "1996-07-11", 1: "1996", 2: "07", 3: "11", groups: { year: '1996', month: '07', day: '11' }, index: 0, input: "1996-07-11", length: 4]

零宽断言

  • 当某个值为条件,而我们又不想让他被替换,这时可以使用零宽断言

  • 零宽断言分 正向负向,都分别有 肯定否定

    • 正向:条件在后
    • 负向:条件在前
    • 肯定:符合要求替换
    • 否定:不符合要求替换
  • 结合 demo 理解嗷~

正向零宽断言

const str = 'iphone4iphone5iphone10iphoneNumber'; // '苹果4苹果5苹果10iphoneNumber'
const reg1 = /iphone\d+/g;
console.log(str.replace(reg1, '苹果')); // 苹果苹果苹果iphoneNumber

// \d+ 是条件,但我又不想让他被替换
// 肯定
const reg2 = /iphone(?=\d+)/g;
console.log(str.replace(reg2, '苹果')); // 苹果4苹果5苹果10iphoneNumber
// 否定
const reg3 = /iphone(?!\d+)/g;
console.log(str.replace(reg3, '苹果')); // iphone4iphone5iphone10苹果Number

负向零宽断言

const str = '10px20px30pxipx'; // 10像素20像素30像素ipx
const reg1 = /\d+px/g;
console.log(str.replace(reg1, '像素')); // 正向零宽断言

// \d+ 是条件,但我又不想让他被替换
// 肯定
const reg2 = /(?<=\d+)px/g;
console.log(str.replace(reg2, '像素')); // 10像素20像素30像素ipx
// 否定
const reg3 = /(?<!\d+)px/g;
console.log(str.replace(reg3, '像素')); // 10px20px30pxi像素