还不会 JavaScript 中的正则表达式 ? 一篇文章带你轻松入门 !

还不会 JavaScript 中的正则表达式 ? 一篇文章带你轻松入门 !

文章目录

一、前言 :

  • 笔者对正则表达式的态度一直是非敌非友的, 这不仅仅是缘于正则表达式本身的枯燥与不易理解, 还有一些自身的实力与心态方面的原因。
  • 但不可否认, 正则这门 “语言”, 掌握后一定是后续开发的一大利器, 虽然笔者研究了一阵子, 但却什么都不会😂
  • 谨以此文, 抛砖引玉。将笔者学习到的记录在案, 供后续查阅(如有错误地方请大家在留言区指正一下, 笔者将不胜感激) !

二、阅前须知 :

  • 本文主要参考了 "珠峰培训" 的官方教材 / 讲义 和 老姚的《JavaScript 正则表达式迷你书》, 如果涉及版权问题请及时与笔者取得联系。
  • 正如本文是站在巨人的肩膀上产生的, 就连整个篇幅都与老姚的书别无二致, 与其说这是一篇笔者的 js 关于正则的文章, 倒不如说是笔者对老姚的书以及 “珠峰培训” 讲义的粗浅解读。
  • 下面开启阅读之旅✨

三、步入正题

1. 什么是正则 ?

正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。
— 百度百科

在笔者看来简单说, 正则表达式是一种字符串匹配逻辑。
既, 提前指定一套匹配规则, 目标字符串进行匹配与捕获 。

2. 怎么使用正则 ?

-1). 正则表达式的创建方式
①. 字面量式
let reg_1 = /^$/;
console.log(reg); // /^$/
②. 构造函数式
let reg_2 = new RegExp("/^$/");
console.log(reg); // /^$/

需要了解的点 :

  • 通过字面量创建的正则与通过构造函数创建的正则同样属于 RegExp 构造函数的实例。
console.log(reg_1 instanceof RegExp); // true
console.log(reg_2 instanceof RegExp); // true
  • RegExp 构造函数有第二个参数, 第二个参数就是为了给借助第一个参数创建的正则添加修饰符(后面会说到)的。
let reg = new RegExp("^$", "img");
console.log(reg); // /^$/gim
  • RegExp 的第一个参数不仅仅可以是字符串参数也可以是一个字面量正则。如果是正则字面量做第一个参数且自带修饰符的话, RegExp 构造函数的二参提供的修饰符默认会将字面量自带的给 remove 掉。
let reg = new RegExp(/^$/g, "m");
console.log(reg); // /^$/m
  • 在 JavaScript 中的字符串中也存在需要转义的现象 :
console.log("\n");
console.log("\\n");
console.log("\\");
console.log("\(\)");

在这里插入图片描述
所以通过构造函数式来撞见正则的时候就要留意 1 参, 字符串参数这个位置中的元素是否需要转义。

  • 字面量式与构造函数式的使用场景 :
    • 字面量式 : 通常使用。
    • 构造函数式 : 仅在所创建的正则是一个动态的正则时需要通过字符串拼接来完成正则的创建的时候使用。
-2). 字符匹配
①. 两种模糊匹配
  • 横向模糊匹配 :
    匹配的字符串的长度是不固定的【实现方式主要是使用量词】
// 横向模糊匹配
let reg = /zfpx\d{1,}years/g;
let str = "zfpx10years zfpx20years";
console.log(str.match(reg)); // ["zfpx10years", "zfpx20years"]
  • 纵向模糊匹配 :
    正则表达式所匹配的字符串具体到某一位字符的时候, 可以不是某个确定的字符, 可以有多种可能【实现方式主要是使用字符组(簇)】
// 纵向模糊匹配
reg = /zfpx[1,2,3]0*years/mg;
str = "zfpx1000years zfpx2000years";
console.log(str.match(reg)); // ["zfpx1000years", "zfpx2000years"]
②. 字符组(簇)
  • 虽然叫字符组, 但仅仅是一个其中的一个字符(含有稍许的 “或” 的含义)。
// 认识字符组
let reg = /zfpx[0-9]\d*years/;
let str = "zfpx02years";
console.log(reg.exec(str)); // ["zfpx02years", index: 0, input: "zfpx02years", groups: undefined]
reg = /zfpx[135]\d*years/;
console.log(reg.exec(str)); // null
  • 范围表示法 : 【4 ~ 8 或 f ~ k 或 O ~ T】=> [45678efghijkOPQRST]
let reg = /zfpx[4-8f-kO-T]years/mg;
let str = "zfpx4years zfpxayears zfpxkyears";
console.log(str.match(reg)); // ["zfpx4years", "zfpxkyears"]
  • 如果需要 ’ - ’ 没意义 : 尽量将 ’ -’ 放在字符组的开头或者结尾。
let reg = /zfpx[25-]years/mg;
let str = "zfpx3years zfpx5years zfpx-years";
console.log(str.match(reg)); // ["zfpx5years", "zfpx-years"]
  • 排除字符组 (不包括 [ … ]中的任何一个), 也叫反字符集合【[^0-9] 除数字外】
    匹配除分组内出现的元素外的元素。
let reg = /zfpx[^25-]years/mg;
let str = "zfpx3years zfpx5years zfpx-years";
console.log(str.match(reg)); // ["zfpx3years"]
  • 有了上述的字符组的概念, 一些元字符就好理解了 : 下面是一些字符组的简写形式 :
元字符对应的字符组
\d表示 [0-9] 一位数字
\D表示 [^0-9] 除数字外的任意一个字符
\w表示 [0-9a-zA-Z_] 数字、字母、下划线 任意一个字符
\W表示 [^0-9a-zA-Z_] 除数字、字母、下划线 外的任意一个字符
\s表示 [\t\v\n\r\f] 表示空白符 -> 水平制表符、垂直制表符、换行符、回车符、换页符、空格
\S表示 [^\t\v\n\r\f] 表示非空白符
.表示 [^\n\r\u2028\u2029] 通配符, 几乎匹配任意字符 (换行符、回车符、行分隔符、段分隔符) 除外
匹配任意字符[\s\S]、[\d\D]、[\w\W]、[^]
③. 量词
  • 贪婪量词 :
贪婪量词释义
{n}必出现 n 次
{n, m}n ~ m 次(包括 n 次, 也包括 m 次)
{n, }至少 n 次(包括 n 次)
?0 ~ 1 次, (包括 0 次, 也包括 1 次) => 相当于 : {0,1}
+至少出现一次 {1,}, (包括 1 次) => 相当于 : {1,}
*出现任意次(可以不出现) => 相当于{0,}
  • 惰性量词 :
惰性量词释义
{n, m}?匹配 {n} 个
{n,}?匹配 {n} 个
??匹配 {0} 个
*?匹配 {0} 个
+?匹配 {1} 个

量词实际上就是规定前面的字符连续出现的次数, 至于为什么有贪婪量词惰性量词之分, 是完全因为正则的匹配模式是贪婪模式, 也就是尽可能多的去匹配(通常都是借助 贪婪量词 来实现的), 但有些是否可能不需要贪婪的匹配, 所以在那些贪婪量词后面加个 ? 就限定了量词按照尽量少的原则去匹配, 也可以称这种匹配模式为 惰性模式 (实际上就是借助惰性量词来实现的)。

贪婪匹配

let reg = /\d{2,5}/g;
let str = "123 456789 753951258";
console.log(str.match(reg)); // ["123", "45678", "75395", "1258"]

惰性匹配

let reg = /\d{2,5}?/g;
let str = "123 456789 753951258";
console.log(str.match(reg)); // ["12", "45", "67", "89", "75", "39", "51", "25"]
④. 多选分支

多选分支其实就是正则中的或者,

// 单个字符
let reg = /1|6/g;
let str = "12s23s56s123s122s125s126s223s256s12256s356s";
console.log(str.match(reg)); // ["1", "6", "1", "1", "1", "1", "6", "6", "1", "6", "6"]

=> 但是多选分支通常都是配合着分组来使用 !
为什么要配合分组来使用 ?
答 : 这就涉及到正则表达式优先级的问题。正则中 | 分支结构的优先级最低, 所以两边的元素会被当成两个整体, 借助分组可以提高多选分支的优先级从而达到我们的预期。
举个栗子(我们要匹配 12、23、56) :

  • 不配合使用分组的情况下 :
let reg = /^12|23|56$/;
console.log(reg.test("12")); // true
console.log(reg.test("23")); // true
console.log(reg.test("56")); // true
console.log(reg.test("123")); // true
console.log(reg.test("1235")); // true
console.log(reg.test("223")); // true
console.log(reg.test("5235")); // true
console.log(reg.test("122")); // true
console.log(reg.test("156")); // true
console.log(reg.test("1356")); // true
console.log(reg.test("223")); // true
console.log(reg.test("1225")); // true
console.log(reg.test("1226")); // true
console.log(reg.test(str)); // true

这跟预期的可能不太一样, 我们是想要匹配 13、23、56, 可是现在匹配的这些是什么gui ?
实际上, 这是必然的, 针对于这段正则而言 /^12|23|56$/ , 所要匹配的是 : 以 12 开头的中间是 23 的结尾是 56 的【3 者是或者的关系哦 ~】。
所以上述的这些就都会返回 true 了, 那代码证实一下, 看看是不是这么匹配的 ~

let reg = /^12|23|56$/;
console.log(reg.exec("12")); // ["12", index: 0, input: "12", groups: undefined]
console.log(reg.exec("23")); // ["23", index: 0, input: "23", groups: undefined]
console.log(reg.exec("56")); // [ '56', index: 0, input: '56', groups: undefined ]
console.log(reg.exec("123")); // ["12", index: 0, input: "123", groups: undefined]
console.log(reg.exec("1235")); // [ '12', index: 0, input: '1235', groups: undefined ]
console.log(reg.exec("223")); // [ '23', index: 1, input: '223', groups: undefined ]
console.log(reg.exec("5235")); // [ '23', index: 1, input: '5235', groups: undefined ]
console.log(reg.exec("122")); // ["12", index: 0, input: "123", groups: undefined]
console.log(reg.exec("156")); // ["56", index: 1, input: "156", groups: undefined]
console.log(reg.exec("1356")); // ["56", index: 2, input: "1356", groups: undefined]
console.log(reg.exec("223")); // ["23", index: 1, input: "223", groups: undefined]
console.log(reg.exec("1225")); // ["12", index: 0, input: "1225", groups: undefined]
console.log(reg.exec("1226")); // ["12", index: 0, input: "1226", groups: undefined]
  • 配合使用分组的情况下 :
let reg = /^(12|23|56)$/;
console.log(reg.test("12")); // true
console.log(reg.test("23")); // true
console.log(reg.test("56")); // true
console.log(reg.test("123")); // false
console.log(reg.test("1235")); // false
console.log(reg.test("223")); // false
console.log(reg.test("5235")); // false
console.log(reg.test("122")); // false
console.log(reg.test("156")); // false
console.log(reg.test("1356")); // false
console.log(reg.test("223")); // false
console.log(reg.test("1225")); // false
console.log(reg.test("1226")); // false

这样通过分组提升了多选分支优先级, 则改变了多选分支的匹配环境, 使其先在分组【(…)】 内部先规定好了再去结合外边的 ^ 与 $, 此时多选分支代表匹配的是 /^(12)$//^(23)$//^(56)$/ , 所以这次的匹配一切正常。
=> 多选分支也是遵循惰性匹配的。

let reg = /book|bookcase/;
console.log(reg.exec("book")); // ["book", index: 0, input: "book", groups: undefined]
console.log(reg.exec("bookcase")); // ["book", index: 0, input: "bookcase", groups: undefined]
console.log(reg.test("book")); // true
console.log(reg.test("bookcase")); // true

仅仅匹配到 book 就结束了, 哪怕当前字符串为 bookcase 也仅匹配到 book。
解决此现象可将 bookcase 提前。

let reg = /bookcase|book/;
console.log(reg.exec("book")); // ["book", index: 0, input: "book", groups: undefined]
console.log(reg.exec("bookcase")); // ["bookcase", index: 0, input: "bookcase", groups: undefined]
-3). 位置匹配
①. 什么是位置 ?

"SKY" 来说, 就有 4 个位置。
在这里插入图片描述

②. 匹配位置

在 ES5 语法中有 6 个锚可供我们来匹配位置, 他们分别是 : ^【脱字符】$【美元符】\b【单词边界】\B【非单词边界】(?=p)【正向先行断言, 也叫正向预查】(?!p【负向先行断言, 也叫负向预查】)

  • ^ 匹配字符串的开头
  • $ 匹配字符串的结尾
  • \b 匹配 \w与\W 、\w与^、\w与$ 之间的位置
  • \B 与 \b 相反
  • (?=p) 与 (?!p) 实际上都是匹配前面那个位置, 只不过二者匹配的内容不同, 前者匹配的是后面必须是 "p" 的前面那个位置、后者匹配的是后面不能是"p" 的前面那个位置。

^ 与 $

 // 将字符串的开头与结尾替换为 & 符
let reg = /^|$/g;
let str = "天地玄黄 宇宙洪荒\n纵有千古";
reg = /^|$/mg;
console.log(str.replace(reg, "&"));
/*
     &天地玄黄 宇宙洪荒&
     &纵有千古&
 */

\b与\B

let reg = /\b/g;
let str = "[JS] FruitJ_01.mp4";
console.log(str.replace(reg, "&")); // [&JS&] &FruitJ_01&.&mp4&
reg = /\B/g;
console.log(str.replace(reg, "&")); // &[J&S]& F&r&u&i&t&J&_&0&1.m&p&4

(?=p)与(?!p)

let reg = /(?=u)/g;
let str = "FruitJ";
console.log(str.replace(reg, "&")); // Fr&uitJ
reg = /(?!u)/g;
console.log(str.replace(reg, "&")); // &F&ru&i&t&J&

如果弄清楚上面笔者阐述的内容, 到这里就会体会到正则是如何匹配位置的, 这段代码里, 正向先行断言部分比较好理解, 不好理解的就是负向先行断言那块, 为什么变成了 &F&ru&i&t&J& 而不是 F&ru&i&t&J ?
是因为上面贴张图笔者已经说明了, 在字符串的开头与结尾都是有 “位置” 的, 开头那个位置后面是 F 不是 u 所以符合模式规则就替换了一个 #, 结尾也是一样, J 后面是结尾那个位置而不是 u , 所以也替换一个 # 。最后就变成了 &F&ru&i&t&J&
再来段儿代码体会体会 :

let reg = /zfpx(?=good)/;
str = "szfpxgoods";
console.log(str.match(reg)); // [ 'zfpx', index: 1, input: 'szfpxgoods', groups: undefined ]
reg = /zfpx(?!good)/;
str = "szfpxbooks";
console.log(str.match(reg)); // [ 'zfpx', index: 1, input: 'szfpxbooks', groups: undefined ]
③. 位置特性

实际上位置啊位置就可以直观的表现为这样 :

// FruitJ
"" + "F" + "r" + "u" + "i" + "t" + "J" + ""
↑        ↑     ↑     ↑     ↑     ↑       ↑
-4). 分组和分支结构

分组的作用 :

  • 提升优先级, 分组内的正则是一个整体。
  • 提供引用。
①. 分组和分支结构

分组
我们都知道 /a+/ 是匹配多个连续出现的 a。但是如果要匹配连续出现的 “ab” 则必须这样写 : /(ab)+/

  • 不使用分组 :
let reg = /ab+/g;
let str = "ababababbbbabbababababbbbbabbbbbaba";
console.log(str.match(reg)); // ['ab', 'ab', 'ab', 'abbbb', 'abb', 'ab', 'ab', 'ab', 'abbbbb', 'abbbbb','ab']

匹配的结果有多个 “b” 的情况是因为贪婪量词 + 是单独作用在了 “b” 上

  • 使用分组 :
let reg = /(ab)+/g;
let str = "ababababbbbabbababababbbbbabbbbbaba";
console.log(str.match(reg)); // [ 'abababab', 'ab', 'abababab', 'ab', 'ab' ]

这样匹配到的就是 “ab” 了, 因为 + 作用在了 “ab” 这个整体上。
这样怪怪的, 能不能匹配单个的 “ab” “ab” 这样形式 ? 还记得惰性量词吗 ?

let reg = /(ab)+?/g;
let str = "ababababbbbabbababababbbbbabbbbbaba";
console.log(str.match(reg)); // ['ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab']

+? 就会尽可能少的匹配, 所以不会一次性匹配多个 “ab” 。只会一个一个的匹配。
分支结构
我们已经知道了分组会提升多选分支的优先级 :

  • 不使用分组情况下 :
let reg = /zfpx is best|school do you like \?/g;
let str = "zfpx is best school do you like ?";
console.log(str.match(reg)); // [ 'zfpx is best', ' school do you like ?' ]
  • 使用分组的情况下 :
reg = /zfpx is (best|school) do you like \?/;
str = "zfpx is best do you like ?";
console.log(reg.exec(str));
/*
   [
       'zfpx is best do you like ?',
       'best',
       index: 0,
       input: 'zfpx is best do you like ?',
       groups: undefined
   ]
*/
str = "zfpx is school do you like ?";
console.log(reg.exec(str));
/*
   [
       'zfpx is school do you like ?',
       'school',
       index: 0,
       input: 'zfpx is school do you like ?',
       groups: undefined
   ]
*/
②. 分组引用
console.log(/(\d)/.exec("123"));

在这里插入图片描述
实际上图中画红框的地方就是匹配到的分组中的内容, 言外之意分组中的内容可以被我们捕获到, 所以基于这一点再配合 String.prototype.replace 方法可以实现功能强大的替换。

  • 那我们可不可以在外界引用这个分组中的内容呢 ?
let reg = /(\d)/;
let str = "123";
console.log(reg.exec(str)); // ["1", "1", index: 0, input: "123"]
console.log(RegExp.$1); // 1

通过 RegExp 成功的进行了引用。
但是这仅仅是在外部引用,。

③. 反向引用
  • 实际上正则表达式本身就可以进行引用(反向引用)
    看一个需求: 【匹配这样一个正则 2016-06-12、 2016/06/12、 2016.06.12】
    很容易写出这样的一个正则表达式 :
let reg = /^\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}$/;
console.log(reg.exec("2016-08-09")); // ["2016-08-09", "-", "-", index: 0, input: "2016-08-09"]
console.log(reg.exec("2016/08-09")); // ["2016-08-09", "-", "-", index: 0, input: "2016-08-09"]

在这里插入图片描述
这样虽然可以满足需求, 但是不符合规则的字符串也被匹配进来了, 所以这个时候就要对第一次匹配的那个分组进行反向引用, 这样才可以符号的保证前后一致 :

let reg = /^\d{4}(-|\/|\.)\d{2}\1\d{2}$/;
console.log(reg.exec("2016-08-09")); // ["2016-08-09", "-", index: 0, input: "2016-08-09"]
console.log(reg.exec("2016/08-09")); // null

在这里插入图片描述
实际上 \1 就是引用的前面分组的匹配的内容, 前面分组匹配的符号是啥, 我这引用的这个符号就是啥, 所以实现了前后符号的一致。
在这里插入图片描述

  • 那如果是分组的嵌套呢 ? 这个如果引用的话该怎么搞 ?
let reg = /^((\d)(\d(\d)))\1\2\3\4$/;
let str = "4564564566";
console.log(str.match(reg)); // [ '4564564566', '456', '4', '56', '6', index: 0, input: '4564564566', groups: undefined ]
console.log(reg.test(str)); // true
console.log(RegExp.$1); // 456
console.log(RegExp.$2); // 4
console.log(RegExp.$3); // 56
console.log(RegExp.$4); // 6

如果嵌套分组则在引用的时候遵循的是 : 先总后分 -> 先左后右 -> 由外到里
所以此正则匹配的配图为 :
在这里插入图片描述

  • \11 代表着什么 ? 还能正常的使用反向引用的功能吗 ?
let reg = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(&)(\$)\11/;
let str = "123456789&$$";
console.log(str.match(reg)); // [ '123456789&$$', '1', '2', '3', '4', '5', '6', '7', '8', '9', '&', '$', index: 0, input: '123456789&$$', groups: undefined ]

在这里插入图片描述
经代码证明即使超过 10。 \11 仍然可以正常使用。

  • 引用不存在的分组会怎样 ?
let reg = /(1)(2)\3/;
let str = "12\3";
console.log(str.match(reg)); // [ '12\u0003', '1', '2', index: 0, input: '12\u0003', groups: undefined ]
console.log(str.match(reg).input); // 12

如果引用的分组不存在, 则默认会将该字符转义, 转义成对应的 unicode 码值, 也就是说此时的正则应该为 : /(1)(2)\u0003/

let reg = /(1)(2)\u0003/;
console.log(str.match(reg)); // ["12", "1", "2", index: 0, input: "12"]
console.log(str.match(reg).input); // 12
  • 如果分组后面有量词会发生什么 ?
let reg = /(\d)+/g;
let str = "123456";
console.log(reg.exec(str)); // [ '123456', '6', index: 0, input: '123456', groups: undefined ]

如果没有量词那最后匹配到的小分组有 6 个 分别是 : “1”、“2”、“3”、“4”、“5”、“6”

let reg = /(\d)/g;
let str = "123456";
console.log(reg.exec(str));
console.log(reg.exec(str));
console.log(reg.exec(str));
console.log(reg.exec(str));
console.log(reg.exec(str));
console.log(reg.exec(str));

在这里插入图片描述
如果在分组后面跟着量词, 则分组只会在最后匹配到的时候捕获一次(也即 : 分组最终匹配的数据是最后一次的匹配)
在这里插入图片描述

④. 匹配不捕获(?: )

如果我们只想使用分组的提升优先级的作用, 而不想对其引用呢 ?
可以通过匹配不捕获来搞
正常情况下 :

let reg = /(\d)/g;
let str = "123";
console.log(reg.exec(str)); // [ '1', '1', index: 0, input: '123', groups: undefined ]

在这里插入图片描述
匹配不捕获 :

let reg = /(?:\d)/g;
let str = "123";
console.log(reg.exec(str)); // [ '1', index: 0, input: '123', groups: undefined ]

在这里插入图片描述
使用了 ?: 后分组将不再参与捕获。

-5). 回溯法原理

回溯 : 就是正则表达式在参与匹配时的一种 "试探" 的机制。

①. 没有回溯的匹配
let reg = /ab{1,3}c/;
let str = "abbbc";
console.log(reg.exec(str)); // [ 'abbbc', index: 0, input: 'abbbc', groups: undefined ]

在这里插入图片描述

②. 有回溯的匹配

例一、

let reg = /ab{1,3}c/;
let str = "abbc";
console.log(reg.exec(reg.exec(str))); // [ 'abbc', index: 0, input: 'abbc', groups: undefined ]

在这里插入图片描述
此匹配模式在匹配字符串的时候产生了回溯, 因为当第二个 b 匹配完毕后, 紧接着匹配第三个 b , 但是下一个字符却是 c , 所以产生回溯, 回退到匹配完 2 个 b 的状态, 接着匹配 c …
再来一个
例二、

let reg = /ab{1,3}bbc/;
let str = "abbbc";
console.log(reg.exec(str)); // [ 'abbbc', index: 0, input: 'abbbc', groups: undefined ]

此正则在匹配 “abbbc” 的时候也发生了回溯。
图解 :
在这里插入图片描述

③. 常见的回溯形式
  • 贪婪量词
    大部分的回溯行为的发生都与使用贪婪量词有关。
    譬如说 : /b{1,3}/ , 首先尝试匹配 bbb, 如果没有合适的就吐出一个 b, 匹配 bb, 如果再没有合适的就再吐出一个, 匹配 b , 如果还没有合适的则报错。
    当多个贪婪量词挨着并且彼此间有冲突(先下手为强)
let reg = /(\d{1,3})(\d{1,3})/;
let str = "12345";
console.log(reg.exec(str)); // [ '12345', '123', '45', index: 0, input: '12345', groups: undefined ]

由打印数据可以看出 : 第一个分组匹配到的是 123 , 第二个分组匹配到的是 45

  • 惰性量词
    能够出发回溯的并非只有贪婪量词, 惰性量词也会触发回溯行为。
let reg = /(\d{1,3}?)(\d{1,3})$/;
let str = "12345";
console.log(reg.exec(str)); // [ '12345', '12', '345', index: 0, input: '12345', groups: undefined ]

图解 :
在这里插入图片描述

  • 分支结构
    多选分支有时也会触发回溯
let reg = /^(can|candy)$/;
let str = "candy";
console.log(reg.exec(str)); // [ 'candy', 'candy', index: 0, input: 'candy', groups: undefined ]

在这里插入图片描述

-6). 正则表达式的拆分
①. 结构和操作符

想要更好的对正则表达式进行拆分则必须事先了解, 正则表达式的优先级。

/ab?(c|de*)+|fg/

由于管道符的优先级最低, 所以 ab?(c|de*)+ 是一个整体 fg 是另一个整体
由于括号的优先级较高, 所以 ab? 是一个整体 (c|de*)+ 是另一个整体
a 是一个整体 b? 是一个整体; c 是一个整体 de* 是另一个整体

图解 :
在这里插入图片描述

②. 注意要点
  • 匹配字符串整体的问题 :
    我们都知道在正则表达式的开头和结尾分别加上 ^ 和 $, 就意味着要匹配整个字符串。但是有些时候会产生与预期不同的效果, 譬如说 : 要匹配 ab 或者 cd , 可能想都不用想 => /^ab|cd$/。但是由于位置操作符的优先级要比管道符高, 所以管道符左右两边都是一个整体, 所以此正则匹配的是 以 ab 开头的以cd结尾的(会产生与预期不同的效果)。
 let reg = /^ab|cd$/; // 此正则匹配的是 以 ab 开头或者以 cd 结尾
 let str = "abssscde";
 console.log(reg.exec(str)); // [ 'ab', index: 0, input: 'abssscde', groups: undefined ]
 str = "eabssscd";
 console.log(reg.exec(str)); // [ 'cd', index: 3, input: 'eabssscd', groups: undefined ]

在这里插入图片描述
所以要借助分组来提升多选分支的优先级

let str = "eabssscd";
let reg = /^(ab|cd)$/; // 此正则匹配的是 以 ab 开头与结尾的 或者 以cd 开头与结尾的
console.log(reg.exec(str)); // null
str = "ab";
console.log(reg.exec(str)); // [ 'ab', 'ab', index: 0, input: 'ab', groups: undefined ]
str = "cd";
console.log(reg.exec(str)); // [ 'cd', 'cd', index: 0, input: 'cd', groups: undefined ]

在这里插入图片描述

  • 量词连缀问题 :
    在 JavaScript 领域的正则中, 除了 ? 最为惰性匹配的标志时可以加到其他的量词后面, 剩下其他任何量词都不能加到其他量词后, 否则会报错。
let reg = /\d{1,3}+/;
let str = "12345678";
console.log(reg.exec(str)); // SyntaxError: Invalid regular expression: /\d{1,3}+/: Nothing to repeat

报错信息大概描述的是, + 前面是一个量词, 不是一个作用主体, 所以不能(没啥)重复的。

let reg = /(\d{1,3})+/; 
console.log(reg.exec(str)); // [ '12345678', '78', index: 0, input: '12345678', groups: undefined ]

使用分组后, 这次 + 前面是一个分组, 该分组作为一个整体就成为了, + 的作用主体。

  • 元字符转义问题 :
    这些字符如果代表本身意思的时候需要转义 :
    ^$.*+?|\/()[]{}=!:-
let reg = /\^\$\[\]\(\)\|\*\+\?\.\\\{\}=!:-/;
let str = "^$[]()|*+?.\\{}=!:-";
console.log(reg.exec(str)); // [ '^$[]()|*+?.\\{}=!:-', index: 0, input: '^$[]()|*+?.\\{}=!:-', groups: undefined ]

在这里插入图片描述
字符组中的元字符就代表本身的含义, 但是有两个是需要转义的。
字符组中的 ^-

  1. =、!、:、-、, 不用在特殊结构中, 基本不需要转义
  2. 括号也需要前后转义 ()
  3. ^、$、.、*、+、?、|、/、\ 基本上只要不在字符组里面都需要转义
③. 分析一个稍复杂的正则表达式

分析一下 : 匹配 IPV4 地址的正则表达式 :
/^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/

答 : 找出多选分支和分组将他们一个个揪出来。

A: ((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5]).){3} 是一个整体
B: (0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5]) 是另一个整体
拆分 A: -> (0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5]) 是一个整体 \. 是另一个整体
go on -> 0{0,2}\d 是一个整体 、0?\d{2} 是一个整体、1\d{2}是一个整体、2[0-4]\d 是一个整体、25[0-5] 是一个整体
拆分 B: -> (0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])
go on -> 0{0,2}\d 是一个整体、0?\d{2} 是一个整体、1\d{2} 是一个整体、2[0-4]\d 是一个整体、25[0-5] 是一个整体

根据拆分的内容, 手写一个 IPV4 地址 => 03.076.255.253


let reg = /^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/;
let str = "03.076.255.253";
console.log(reg.exec(str)); // [ '03.076.255.253', '255.', '255', '253', index: 0, input: '03.076.255.253', groups: undefined ]

在这里插入图片描述

-7). 正则表达式的构建

上述小节已经介绍了如何利用正则表达式的优先级来对其进行拆分, 那么本节的关键就是介绍如何正确保险的构建一个复杂的正则表达式。

①. 构建前的考量

在构建一个正则表达式之前, 要先考虑以下几点 :

  • 匹配预期字符串
  • 不匹配非预期字符串
  • 可读性和可维护性
  • 效率
②. 构建的前提

-1). 是否能使用正则 ?

  • 不得不说, 如果看到一个有关字符串的需求笔者会首先考虑一下正则的相关实现【笔者认为正则比较严谨, 有些时候比较省事】, 实际上这不是一个好习惯, 如果内置的字符串方法可以不复杂的满足需求就不要去构建一个正则表达式。

  • 另外有些简单的需求正则表达式也无法去匹配; 例如 : 要匹配这样的一个字符串 1010010001… , 笔者第一次从书上看也觉得简单, 结果根本无法构建可以与之匹配的正确的正则。

-2). 是否有必要使用正则 ?
还是那句话 : String.prototype 上的方法可以满足, 就不用正则, 如果满足不了再去构建一个正则表达式。
举几个栗子 :

  • 提取 -> 年月日【2017-07-01】

正则来做 :

let reg = /^(\d{4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/;
let str = "2020-04-05";
let res = reg.exec(str);
console.log(res[1], res[2], res[3]); // 2020 04 05

split 来做 :

console.log(...str.split("-")); // 2020 04 05
  • 判断是否有问号

正则来做 :

reg = /\?/g; 
str = "How do you do ?";
console.log(reg.test(str)); // true

includes 来做 :

console.log(str.includes("?")); // true
  • 获取子串 【JavaScript】

正则来做 :

let reg = /^\w{4}(\w+)$/;
let str = "JavaScript";
console.log(reg.exec(str)[1]); // Script

slice 来做 :

console.log(str.slice(4)); // Script

-3). 是否有必要构建一个复杂的正则 ?
如果正则过于复杂, 并且长度较长不易维护, 则可以将其进行拆分, 拆成多个小正则。

③. 构建时的准确性

构建一个稍复杂的正则表达式时, 可以 先分着 写, 然后合在一起, 再者提取公共部分, 最后再实现优化。
举个栗子 :

  • 匹配如下格式的固定电话号码(规则 : “0” 开头的 3 到 4 位数字和非 “0” 开头的 7 到 8 位数字) :
A. 055188888888
B. 0551-88888888
C. (0551)88888888

分身 :
A. /0\d{2,3}[1-9]\d{6,7}/
B. /0\d{2,3}-[1-9]\d{6,7}/
C. /(0\d{2,3})[1-9]\d{6,7}/

合体 :
/^(0\d{2,3}[1-9]\d{6,7}|0\d{2,3}-[1-9]\d{6,7}|(0\d{2,3})[1-9]\d{6,7})$/

化形 :
/^(0\d{2,3}|0\d{2,3}-|(0\d{2,3}))[1-9]\d{6,7}$/

渡劫 :
/^(0\d{2,3}-?|(0\d{2,3}))[1-9]\d{6,7}$/

测试 :

let reg = /^(0\d{2,3}\-?|\(0\d{2,3}\))[1-9]\d{6,7}$/;
let str = "055188888888";
console.log(reg.exec(str)); // [ '055188888888', '0551', index: 0, input: '055188888888', groups: undefined ]
str = "0551-88888888";
console.log(reg.exec(str)); // [ '0551-88888888', '0551-', index: 0, input: '0551-88888888', groups: undefined ]
str = "(0551)88888888";
console.log(reg.exec(str)); // [ '(0551)88888888', '(0551)', index: 0, input: '(0551)88888888', groups: undefined ]
str = "(0551-88888888"; 
console.log(reg.exec(str)); // null

在这里插入图片描述
再举个栗子 :

  • 匹配浮点数 1.23、+1.23、-1.23、10、+10、-10 .2、+.2、-.2
    分析需求 :
    • + 与 - 可以出现也可以不出现, 如果出现只能出现 一次
    • 小数点可以有也可以没有, 如果有只能出现一次
let reg = /^[-+]?\d*(?:\.)?\d+$/;
let str = "1.23";
console.log(reg.exec(str)); // [ '1.23', index: 0, input: '1.23', groups: undefined ]
str = "+1.23";
console.log(reg.exec(str)); // [ '+1.23', index: 0, input: '+1.23', groups: undefined ]
str = "-1.23";
console.log(reg.exec(str)); // [ '-1.23', index: 0, input: '-1.23', groups: undefined ]
str = "10";
console.log(reg.exec(str)); // [ '10', index: 0, input: '10', groups: undefined ]
str = "+10";
console.log(reg.exec(str)); // [ '+10', index: 0, input: '+10', groups: undefined ]
str = "-10";
console.log(reg.exec(str)); // [ '-10', index: 0, input: '-10', groups: undefined ]
str = "0.2";
console.log(reg.exec(str)); // [ '0.2', index: 0, input: '0.2', groups: undefined ]
str = ".2";
console.log(reg.exec(str)); // [ '.2', index: 0, input: '.2', groups: undefined ]
str = "-0.2";
console.log(reg.exec(str)); // [ '-0.2', index: 0, input: '-0.2', groups: undefined ]
str = "-.2";
console.log(reg.exec(str)); // [ '-.2', index: 0, input: '-.2', groups: undefined ]
str = "-.";
console.log(reg.exec(str)); // null
str = "0.";
console.log(reg.exec(str)); // null

此例子的正则也可以写成这样 : /^(?:\-|\+)?\d*(?:\.)?\d+$/

④. 构建后的效率
  • 使用具体的字符组来代替通配符以取消回溯。
    例如 : 匹配字符串 123"abc"456 中的 “abc”
    => 这样写正则 : /".*"/ 效率就不太高, 在匹配的过程中会产生回溯。
    图解 :
    在这里插入图片描述
    => 如果使用字符组 : /"[^"]*"/
    在这里插入图片描述
    二者效率对比 …, 所以尽量使用具体的字符组来代替通配符以取消回溯。

3. JavaScript 对正则的支持

-1). RegExp 的实例属性
  • flags : 获取当前修饰符
  • global : 布尔值, 如果设置了全局修饰符 g 就会被置为 true
  • ignoreCase : 布尔值, 如果设置了忽略大小写修饰符 i 就会被置为 true
  • multiline : 布尔值, 如果设置了匹配多行的修饰符 m 就会被置为 true
  • sticky : 布尔值, 如果设置了"粘连"的修饰符 y 就会被置为 true
  • unicode : 布尔值, 如果设置了"正确处理大于\uFFFF的 Unicode 字符"的修饰符 u 就会被置为 true
  • source : 获取正则表达式的字符串表现形式(但是会忽略行为修饰符【imguy】)。
  • lastIndex : 下一次匹配的起始位置的索引
let reg = /.{0,3}/imguy;
let str = '123asD456def ghi789𠮷';
console.log(reg.exec(str));// [ 'asD', index: 3, input: '123asD456def ghi789' ]
console.log(`reg's g: ${reg.global}`);// reg's g: true
console.log(`reg's i: ${reg.ignoreCase}`);// reg's i: true
console.log(`reg's m: ${reg.multiline}`);// reg's m: true
console.log(`reg's y: ${ reg.sticky }`); // reg's y: true
console.log(`reg's y: ${ reg.unicode }`); // reg's u: true
console.log(`reg's lastIndex: ${reg.lastIndex}`);// reg's lastIndex: 3
console.log(`reg's source: ${reg.source}`); // reg's source: .{0,3}

在这里插入图片描述

-2). RegExp 的静态属性
  • $1 ~ $9 : 分组引用, $1 引用第一个分组匹配的内容, 一直到 $9。
属性简写释义
input$_最近一次参与匹配的字符串
lastMatch$&最近一次的匹配项
lastParen$+最近一次的捕获组
leftContext$`input 字符串中 lastMatch 之前的文本
rightContext$’input 字符串中 lastMatch 之后的文本

不使用简写属性 :

let reg = /\d(\d)+/m;
let str = 'asd123jkl';

console.log(reg.exec(str));// ["123", "3", index: 3, input: "asd123jkl"]
console.log(RegExp.input);// asd123jkl
console.log(RegExp.lastMatch);// 123
console.log(RegExp.lastParen);// 3
console.log(RegExp.leftContext);// asd
console.log(RegExp.rightContext);// jkl

在这里插入图片描述
使用简写属性 :

var reg = /\d(\d)+/m;
var str = 'asd123jkl';

console.log(reg.exec(str));// (2) ["123", "3", index: 3, input: "asd123jkl", groups: undefined]
console.log(RegExp['$_']);// asd123jkl
console.log(RegExp['$&']);// 123
console.log(RegExp['$+']);// 3
console.log(RegExp['$`']);// asd
console.log(RegExp["$'"]);// jkl

在这里插入图片描述

-3). RegExp.prototype 上的属性与方法(有用的)
  • test【匹配】:
  • exec【捕获】:
-4). String 包装类对正则表达式的支持
①. match

match方接收一个正则实例, 通过正则实例与目标字符串进行匹配, 将匹配的结果(匹配到的详细信息)捕获并返回。如果参数不是一个正则对象会调用 new RegExp() 隐式的将其转换为一个正则实例。

let str = "s.s";
console.log(str.match(".")); // ["s", index: 0, input: "s.s"]
console.log(str.match("\\.")); // [".", index: 1, input: "s.s"]
②. replace

如果说正则方法中最强大的是 exec, 那么笔者认为在支持正则的这四个字符串方法中最强大的就是 replace 方法。因为在 replace 方法可以匹配到更多的信息, 而且可以做功能强大的替换操作。
正常的使用 :
=> 替换空格【小例子】

void(() => {

    function fn() {
        let str = "     好好学习     ";
        str = str.replace(" ", "");
        console.log(str);
    }
    fn();
})();

好好学习的前后各有 5 个空格, 但这段代码仅仅只是替换了第一个空格。
在这里插入图片描述
使用正则后 :

void(() => {

    function fn() {
        let str = "     好好学习     ";
        str = str.replace(/\s/g, "");
        console.log(str);
    }
    fn();
})();

在这里插入图片描述
第二参数有妙用 :
replace 的第二参数有两种形式, 一种是字符串另一种是回调函数。

  • 先说说这个字符串, 首先这个字符串可以是正常字符串, 同时也可以是 RegExp 静态属性的简写, 也即可以是 $&$`$'$+$1~$99【分组引用】
void(() => {

    function fn() {
        let str = "     好好学习     ";
        str = str.replace(/(\s)/g, "$1");
        console.log(str);
    }
    fn();
})();

在这里插入图片描述
如果不了解这个点, 这么写在代码被执行之前一定会认为结果是这样 : $1$1$1$1$1好好学习$1$1$1$1$1 但刚才提到了, 这里的字符串 $1 是有特殊意义的, 引用的是分组里面的内容(也就是引用的空格), 所以最后的效果和为替换之前一致。

  • 如果第二个参数是回调函数, 那么 replace 方法每匹配到一次就会执行一次回调函数, 而这个回调函数的返回值也就是要替换的值。
    且这个回调函数的参数也是有讲究的, 换句话说使用分组与不使用分组, 反映到回调函数中的参数是不同的。
    • 无分组 :
      在这里插入图片描述
let str = "     好好学习     ";
str = str.replace(/\s/g, (...args) => {
   console.log(args);
   return '$1';
});
console.log(str); 

在这里插入图片描述

    • 有分组 :
      在这里插入图片描述
      在这里插入图片描述
      实际上这种参数形式与 exe 返回的结果是一致的 …。
③. split

split 方法虽然也支持正则实例作为参数但是并不会像 match 方法与 search 方法那样会自动将传进来的字符串类型参数转换为正则实例。

console.log('s.s'.split('.')); // ["s", "s"]
console.log('s2s2s2s2s'.split(/\d/g)); // ["s", "s", "s", "s", "s"]

在这里插入图片描述
需要注意的是 :

  • split 方法有第二个参数, 就是指定分割完毕后返回的数组长度。
console.log('s2s2s2s2s'.split(/\d/g, 2)); // ["s", "s"]

在这里插入图片描述

  • 当第一个参数在正则使用分组后, 在分割完毕的数组中会带有分组中的内容。
console.log('s2s2s2s2s'.split(/(\d)/g)); // ["s", "2", "s", "2", "s", "2", "s", "2", "s"]

在这里插入图片描述

④. search

search 方接收一个正则实例, 通过正则实例与目标字符串进行匹配, 将匹配的结果(匹配到的索引)返回。如果参数不是一个正则对象会调用 new RegExp() 隐式的将其转换为一个正则实例。

let str = "s.s";
console.log(str.search(".")); // 0
console.log(str.search("\\.")); // 1
console.log(new RegExp('.').source); // "."
console.log(new RegExp('\\.').source); // "\."

在这里插入图片描述

4. match 与 exec 的联系和区别

二者都是去目标字符串中捕获符合规则的字符集合 。

  • 二者在处理无分组无全局修饰符的时候, 行为一致
let reg = /\d+/;
let str = "sd1sd1sd111sd1sd1s";
console.log(reg.exec(str)); // [ '1', index: 2, input: 'sd1sd1sd111sd1sd1s', groups: undefined ]
console.log(str.match(reg)); // [ '1', index: 2, input: 'sd1sd1sd111sd1sd1s', groups: undefined ]

在这里插入图片描述

  • 二者在处理带有全局修饰符的时候, match 方法会将所有的结果一次性的捕获到, 但会丢失每次捕获时的详细信息。而 exec 方法不会丢失每次捕获时的详细信息但是不会将所有结果一次性返回。
let reg = /\d+/g;
let str = "sd1sd1sd111sd1sd1s";
console.log(reg.exec(str)); //
console.log(str.match(reg)); //

在这里插入图片描述

  • 二者在处理带有分组的时候行为一致
let reg = /(\d+)/;
let str = "sd1sd1sd111sd1sd1s";
console.log(reg.exec(str)); // 
console.log(str.match(reg)); // 

在这里插入图片描述

  • 二者在处理带有分组和全局修饰符的时候, match 方法会忽视分组, 将大正则的匹配的内容捕获并返回, 但是 exec 方法不会忽视分组, 但不会一次性的将所有捕获的内容返回。
let reg = /(\d+)/g;
let str = "sd1sd1sd111sd1sd1s";
console.log(reg.exec(str)); // 
console.log(str.match(reg)); // 

在这里插入图片描述

5. 实践是检验真理的唯一标准

-1). 匹配 16 进制颜色值

#ffbbad
#Fc01DF
#FFF
#ffE

let reg = /#(?:[a-fA-F0-9]{3}){1,2}/g;
let str = "#FFF #F2F2F2 #F234 #012345";
console.log(str.match(reg)); // ["#FFF", "#F2F2F2", "#F23", "#012345"]

在这里插入图片描述

-2). 匹配日期
  • 匹配日期【yyyy-mm-dd】 :
let reg = /^\d{4}-(0[1-9]|1[0-2])-([0-2][1-9]|[12]0|3[01])$/;
console.log("2020-12-31".match(reg));
console.log("2020-13-31".match(reg));
console.log("2020-12-32".match(reg));
console.log("020-12-31".match(reg));
console.log("2020-2-31".match(reg));
console.log("2020-12-1".match(reg));

在这里插入图片描述

  • 匹配时间 【24 小时制】:
let reg = /^([01]\d|2[0-3]):([0-5][1-9]|[0-5]0)$/;
console.log("00:00".match(reg));
console.log("23:59".match(reg));
console.log("24:00".match(reg));
console.log("12:56".match(reg));
console.log("0:10".match(reg));
console.log("00:1".match(reg));
console.log("0:0".match(reg));

在这里插入图片描述

-3). 匹配 id

=> 匹配 id => <div id="container" class="main"></div>

let reg = /id="[^"]*"/;
console.log(`<div id="container" class="main"></div>`.match(reg));

在这里插入图片描述

-4). 千分符
console.log("123456789".replace(/(?!^)(?=(\d{3})+$)/g, ","));

在这里插入图片描述

-5). 密码验证

=> 密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符。

let reg = /(?!^[a-z]{6,12}$|^[A-Z]{6,12}$|^[0-9]{6,12}$)^[a-zA-Z0-9]{6,12}$/;
console.log("12121212".match(reg));
console.log("sasasasa".match(reg));
console.log("SASASASA".match(reg));
console.log("sa12sa12".match(reg));
console.log("SA12SA12".match(reg));
console.log("saSAsaSA".match(reg));
console.log("saSAsa12".match(reg));

在这里插入图片描述

-6). trim 方法模拟
function myTrim(arg) {
	return arg.replace(/(^\s+|\s+$)/g, "")
}
console.log(myTrim("   1   ")); // "1"

在这里插入图片描述

-7). 将每个单词的首字母大写
console.log("The apple is red !".replace(/(?:^|\b)([a-zA-Z])/g, (_, $1) => $1.toUpperCase()));

在这里插入图片描述

-8). 驼峰命名
let reg = /[_-]([a-zA-Z])/g;
console.log("-moz-transform".replace(reg, (_, $1) => $1.toUpperCase()));
console.log("-ms-transform".replace(reg, (_, $1) => $1.toUpperCase()));
console.log("-o-transform".replace(reg, (_, $1) => $1.toUpperCase()));
console.log("-webkit-transform".replace(reg, (_, $1) => $1.toUpperCase()));
console.log("_moz-transform".replace(reg, (_, $1) => $1.toUpperCase()));

在这里插入图片描述

-9). 匹配成对标签
let reg = /<([^>]+)>([^>]*)?<\/\1>/g;
let str = `
	<h1>Hello JavaScript</h1>
	<p>Hello RegExp</p>
	<h1>Hello World</p>
`;
console.log(str.match(reg)); // ["<h1>Hello JavaScript</h1>", "<p>Hello RegExp</p>"]
str = "<h1>Hello World</p> <h1>Hello JavaScript</h1>";
console.log(str.match(reg)); // ["<h1>Hello JavaScript</h1>"]
str = "<h1>Hello JavaScript</h1> <h1>Hello JavaScript</h1>";
console.log(str.match(reg)); // ["<h1>Hello JavaScript</h1>", "<h1>Hello JavaScript</h1>"]
str = "<h1>Hello JavaScript</h1>";
console.log(str.match(reg)); // ["<h1>Hello JavaScript</h1>"]
str = "<h1>Hello World</p>";
console.log(str.match(reg)); // null

在这里插入图片描述

-10). 匹配身份证

身份证规则 :
在这里插入图片描述
在这里插入图片描述

let reg = /[1-9]\d{5}(?:18|19|20)\d{2}(?:0[1-9]|1[0-2])(?:(?:[0-2][1-9])|[1-3]0|31)\d{3}[0-9Xx]/; 
console.log(reg.exec("13082719980509562x")); // ["13082719980509562x", index: 0, input: "13082719980509562x"]
console.log(reg.exec("13082719980532562x")); // null
console.log(reg.exec("13082719981309562x")); // null

在这里插入图片描述

-11). 匹配手机号

经整理, 当前手机号号段为 :
在这里插入图片描述

let reg = /^1(([38]\d)|(4[579])|(5[0-35-9])|(6[26])|(7[235-8])|(9[25-8]))\d{8}$/;
let str = "17803218829"; // 号段存在的号码
console.log(reg.exec(str)); // ["17803218829", index: 0, input: "17803218829"]
str = "14603218829"; // 号段不存在的号码
console.log(reg.exec(str)); // null

在这里插入图片描述

-12). 匹配邮箱(网上找的一个)
let reg = /^[a-z0-9]+([._\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/;
console.log(reg.exec("1848661762@qq.com"));
console.log(reg.exec("pgsyj@qq.com"));
console.log(reg.exec("xxx@xxx.com.cn"));

在这里插入图片描述

-13). format time【珠峰学到的案例】
-14). format queryString(URI)【珠峰学到的案例】
let url = 'https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=2&tn=baiduhome_pg&wd=%22The%20apple%20is%20red%20!%22.replace(%2C%20)&rsv_spt=1&oq=%25E6%25AD%25A3%25E5%2588%2599%25E5%258C%25B9%25E9%2585%258D%252016%2520%25E8%25BF%259B%25E5%2588%25B6%25E9%25A2%259C%25E8%2589%25B2&rsv_pq=93149ff2000acb15&rsv_t=503d7jBJOz1VH7dJGaVX2g2gq3ufvn3HPGdK21yfoXohSTRdwhjr%2FxXQ0rVjO2EftNC9&rqlang=cn&rsv_enter=1&rsv_dl=tb&rsv_sug3=3&rsv_sug1=1&rsv_sug7=001&rsv_n=2&rsv_sug2=0&inputT=1665964&rsv_sug4=1666347&rsv_sug=9#id';
void((prop) => {
	
	function queryParams() {
		let obj = {};
		this.replace(/([^#&?=]+)=([^#&?=]+)/g, (_, $1, $2) => obj[$1] = $2);
		this.replace(/#([^#&?=]+)/, (_, $1) => obj['HASH'] = $1);
		return obj;
	}
	
	prop.queryParams = queryParams;
})(String.prototype);
console.log(url.queryParams());

在这里插入图片描述
在这里插入图片描述

四、总结与补充

  • 正则是用来干啥的 ? 无非是为了匹配验证捕获元素。但是单拿捕获来说, 它与匹配是分不开的, 匹配在前捕获在后, 单拿匹配来说 : 我们要匹配什么 ? 无非是匹配字符或匹配位置了。
  • 正则很有个性, 又贪婪又懒惰, 什么时候贪 ? 什么时候懒 ? 怎样利用这些特性构建符合自己需求的正则表达式 ? 合理的利用量词就显得尤为重要, 怎样能减少回溯来提高匹配效率, 这就要了解回溯的机制。
  • 上面提到过两个行为修饰符可能以前没有见过, 因为这俩行为修饰符是 ES6 新增的。

y修饰符【粘连】
y 修饰符与 g 修饰符比较相像, 二者都是全局匹配, 但是不同之处在于后者是只要剩余位置中存在匹配就可, 前者是确保匹配必须从剩余的第一个位置开始【就是"粘连"的含义】。

void(() => {
	var s = '123a456a789';
    var r1 = /\d{1,3}/g;
    var r2 = /\d{1,3}/y;

    console.log(r1.exec(s)); // ["123", index: 0, input: "123a456a789"]
    console.log(r1.exec(s)); // ["456", index: 4, input: "123a456a789"]
	console.log(r2.exec(s)); // ["123", index: 0, input: "123a456a789"]
    console.log(r2.exec(s)); // null
})();

在这里插入图片描述

void(() => {
	var s = '12345a6789';
    var r1 = /\d{1,3}/g;
    var r2 = /\d{1,3}/y;

    console.log(r1.exec(s)); // ["123", index: 0, input: "12345a6789"]
    console.log(r1.exec(s)); // ["45", index: 3, input: "12345a6789"]
	console.log(r1.exec(s)); // ["678", index: 6, input: "12345a6789"]
	console.log(r2.exec(s)); // ["123", index: 0, input: "12345a6789"]
    console.log(r2.exec(s)); // ["45", index: 3, input: "12345a6789"]
	console.log(r2.exec(s)); // null
})();

在这里插入图片描述
u修饰符【用来正确处理大于\uFFFF的 Unicode 字符】
点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于0xFFFF的 Unicode 字符,点字符不能识别,必须加上u修饰符。

void(() => {
	let str = '𠮷';
    console.log(/^.$/.exec(str)); //
    console.log(/^.$/u.exec(str)); //
})();

在这里插入图片描述
s 修饰符【这次真的代表一切】
原来的 . 代表全部字符(这么说不严谨它不代表 \n等), 但是 ES8 引入了一个 s 修饰符可以真正实现匹配一切【体验 s 修饰符请到 node 环境, 目前浏览器还不支持】。

void(() => {
	console.log(/./.exec("\n")); // null
	console.log(/./s.exec("\n")); // [ '\n', index: 0, input: '\n', groups: undefined ]
})();

在这里插入图片描述

  • 当 test 与 exec 的应用场景不同但有些时候总会造成疑惑
void(() => {

	let reg = /\d{1,3}/;
	let str = "132456";
	console.log(reg.test(str)); // true
	console.log(reg.exec(str)); // ["132", index: 0, input: "132456"]
})();

上面这段代码为什么会返回 true , 不是规定了只能是 1~3个嘛, 实际上 test 匹配到 1 就结束了, 就返回 true 了。
而 exec 则是严格按照规则去捕获。
如果在首位分别加上位置限定符则会返回 false 。

void(() => {

	let reg = /^\d{1,3}$/;
	let str = "132456";
	console.log(reg.test(str)); // false
})();

在这里插入图片描述

  • 在正则表达式有"全局意味"的修饰符的时候 test 与 exec 通用可能会相互影响。
void(() => {

	let reg = /\d{1,3}/g;
	let str = "132456";
	console.log(reg.lastIndex); // 0
	console.log(reg.test(str)); // true
	console.log(reg.lastIndex); // 3
	console.log(reg.exec(str)); // ["456", index: 3, input: "132456"]
	console.log(reg.lastIndex); // 6
})();

在这里插入图片描述
至于 exec 直接捕获到了 456 而非是 123 的原因是因为 test 在匹配的时候修改了 正则实例上的 lastIndex 属性值, exec 在进行全局意味的捕获的时候依靠的就是这个 lastIndex 值。

并且当本轮匹配完毕后, 会将 lastIndex 重新置为 0 , 重新开始

void(() => {

	let reg = /\d{1,3}/g;
	let str = "132456";
	console.log(reg.lastIndex); // 0
	console.log(reg.exec(str)); // ["132", index: 0, input: "132456"]
	console.log(reg.lastIndex); // 3
	console.log(reg.exec(str)); // ["456", index: 3, input: "132456"]
	console.log(reg.lastIndex); // 6
	console.log(reg.exec(str)); // null
	console.log(reg.lastIndex); // 0
	console.log(reg.exec(str)); // ["132", index: 0, input: "132456"]
})();

在这里插入图片描述

  • 正向后行断言与负向后行断言
    正向先行断言与负向先行断言都是匹配前面那个位置, 而正向后行断言与负向后行断言匹配的则是后面那个位置, 二者不同的只不过是匹配的内容不同罢了
void(() => {

    console.log(/(?<=\$)\d/.exec("$2")); // [ '2', index: 1, input: '$2', groups: undefined ]
    console.log(/(?<=\$)\d/.exec("&2")); // null
    console.log(/(?<!\$)\d/.exec("$2")); // null
    console.log(/(?<!\$)\d/.exec("&2")); // [ '2', index: 1, input: '&2', groups: undefined ]
})();

在这里插入图片描述
不论是正向先行断言与负向先行断言还是正向后行断言与负向后行断言 都是用来匹配位置的, 如果 正向先行断言与负向先行断言 你理解的很好, 那么这个 正向后行断言与负向后行断言 理解起来就 so easy 啦 !

五、附录

1. 元字符

元字符描述
\转义字符
^行首, 如果使用了 m 修饰符则也匹配\n或\r 之后的位置
$行尾,如果使用了 m 修饰符则也匹配\n或\r 之前的位置
.表示 [^\n\r\u2028\u2029] 通配符, 几乎匹配任意字符 (换行符、回车符、行分隔符、段分隔符) 除外
( … )分组
(?: … )匹配不捕获
(?= …)正向先行断言
(?! …)负向先行断言
(?<=)正向后行断言【ES8 引入 目前仅 node 环境支持】
(?<!)负向后行断言【ES8 引入 目前仅 node 环境支持】
xy
[xyz]匹配字符组中任意一个字符(可以简写), ^- 要代表本身含义则需要转义
[^xyz]反字符集合(可以简写)【不匹配字符组中任意一个字符】
\b单词边界; 匹配 \w与^之间的位置 、\w 与 $ 之间的位置、\w与\W 之间的位置
\B与 \b 相反
\d数字
\D非数字
\s看不见的字符包括空格、换行符、换页符、制表符等相当于 [\f\n\r\t\v]
\S可见字符
\w字母(大小写)和数字和下划线, 相当于[0-9a-zA-Z_]
\W与 'w 相反

2. 量词元字符

元字符释义
*至少 0 个 => {0,}
+至少 1 个 => {1,}
?0 到 1 个 => {0,1}
{n}n 个
{n,m}n 到 m 个
{n,}至少 n 个
*?0 个
+?1 个
??0 个
{n,}?n 个
{n,m}n 个

3. 修饰符

行为修饰符全称释义
gglobal全局匹配
mmultiline行首行尾
iignoreCase忽略大小写
ysticky粘连(“全局意味”)
uunicode正确处理大于\uFFFF的 Unicode 字符
sdotAll代表一切单个字符

4. 正则表达式优先级

运算符描述优先级
(), (?: ), (?=), []圆括号和方括号最大
*, +, ?, {n}, {n,}, {n,m}限定符次之
^, $, \任何元字符、任何字符位置和顺序再次
|替换,"或"操作兜底

六、鸣谢

笔者再次谢谢老姚的小书与珠峰培训, 谢谢你们让我接触到这么丰富瑰丽的事物, 谢谢, 谢谢 😃
在这里插入图片描述

七、参考资料

-1). 《JavaScript 正则表达式模拟书 - 老姚》
-2). 《珠峰培训讲义与视频》

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值