js 中的正则:/内/外兼修的艺术

213 阅读7分钟

用 /***/ 写的文档注释连起来都能绕公司一周了? 用 // 写的单行注释连起来能绕金陵一周了 ? 今天,让我们搞点正则学学吧,学好正则,优化代码不再是梦想,dear dalao 们,请继续往下看,我们先念经,再取经。

JS 中正则基本概念梳理

1 . 什么是正则表达式 ?

正则表达式(Regular Expression),又称规则表达式,是用于匹配 字符串 中字符组合的模式

  • 只能用来处理字符串
  • 可以用来验证字符串是否符合某个规则(test),也可以用来获取字符串中符合规则的内容(exec)
  • 字符串原型方法上处理正则表达式的常用方法(replace,match,split)

2.正则表达式由哪些东西组成 ?

正则表达式由 修饰符元字符 组成

  • 元字符:分为普通元字符和特殊元字符及量词元字符,用在 // 里(/元字符/修饰符)
  • 修饰符:用在 //外(/元字符/修饰符)

2.1普通元字符

数字和大小写字母

2.2特殊元字符

字符 含义
^ 匹配输入的开始。如果多行标志被设置为 true,那么也匹配换行符后紧跟的位置。
$ 匹配输入的结束。如果多行标志被设置为 true,那么也匹配换行符前的位置。
\ 转义字符
. (小数点)默认匹配除换行符之外的任何单个字符
\n 换行符
\d 0~9之间的数字
\D 除了0~9 之外的任意字符
\w 数字、字母、下划线(小写w)
\W 除了 数字、字母、下划线 的任意字符
\s 一个空白字符(包含空格、制表符、换行符等
\t 一个制表符(一个TAB键:4个空格
\b 匹配一个单词的边界
x|y x或y
[xyz] x或y或z
[^xy] 除了xy的任意字符
[a-z] a~z 的任意字符 小写的英文字母
[^a-z] 除了a-z 的任意字符
() 1.提升优先级 2.分组捕获 3.分组引用
(?:) 只匹配不捕获
(?=) 正向肯定预查
(?!) 正向否定预查
..... 还有很多,写不动了

2.3 量词元字符

字符 含义
* 匹配前一个表达式 0 次或多次
+ 匹配前面一个表达式 1 次或者多次
? 匹配前面一个表达式 0 次或者 1 次
{n} 前面的字符连续出现 n 次
{n,m} 前面的字符连续出现的范围是 n~m 次
{n,} 前面的字符连续出现 n ~ n+ 次
...... 我还能写,这里只列举以上最常见的几种

3.正则表达式该如何创建 ?

假设我们要创建一个用来匹配数字的表达式

  • 字面量方法 : let reg = /\d+/g
  • new 实例方法 : let reg = new RegExp("\\d+","g") // 字符串中我们需要多加个 \ 来转译 \

4.正则表达式如何使用?

4.1 test 的使用

正则.test(需要检测的字符串),匹配则返回 true , 不匹配则返回 false

4.1.1 注册常用的验证

1.校验用户名

涉及知识点 :

  1. ^ : 匹配输入的开始
  2. $ : 匹配输入的结束
  3. [xyz]:x或y或z
  4. {n,m} : 前面的字符连续出现 n ~ m 次
let name = "彭于晏";
let regName = /[\u4E00-\u9FA5]{2,}/; // 中国人的名字,两个汉字起步嘛
regName.test(name) // true (思考:那少数名族的名字呢? 弗拉基米尔·弗拉基米罗维奇·普京)

let name2 = "弗拉基米尔·弗拉基米罗维奇·普京"
let regNanmes = /^[\u4E00-\u9FA5]{2,10}(·[\u4E00-\u9FA5]{2,10}){0,2}$/
regNanmes.test(name2)


2.校验邮箱

涉及知识点 :

  1. \w : 数字、字母、下划线
  2. * : 前面的字符出现 0 ~ 多次
  3. + : 前面的字符出现 1 ~ 多次
  4. \ : 转译有特殊含义的字符
  5. x | y : x 或 y
  6. () : 第一个作用提升优先级
/** @前面部分 + @ + @后面部分
* 1. 普通邮箱:112cbc@qq.com 112c.b.c@qq.com  112c-b-c@qq.com
* 2. 企业邮箱:cbc@cn-suning.com.cn   
**/

let regEmail =  /^\w+((-\w+)|(\.\w+))*@[0-9a-zA-z]+((\.|-)[0-9a-zA-z]+)*\.[0-9a-zA-z]+$/ // 
let str =  "c.b.c@163cn-suning.com.cn"
regEmail.test(str) //true

4.2 正则捕获 exec 的使用

正则.exec(需要匹配的字符串), 返回符合正则的字符串内容

  • 数组中第一项:本次捕获到的内容
  • 其余项:对应小分组本次单独捕获的内容
  • index:当前捕获内容在字符串中的起始索引
  • input:原始字符串
  • groups:存储分组具名化后内容

4.2.1 . 身份证号码的校验及捕获

涉及知识点 :

  1. () 的第二个作用:分组捕获
  2. (?:) 只匹配不捕获
  3. ?<name> 分组具名化处理
/*
* 18 位身份证号码都是数字,只有最后一位是数字或X
*/

let reg1 = /^\d{17}(\d|X)$/
let str = "32068120300728663X"
reg1.test(str) // true  我们难道只想知道她填的身份证号码对不对吗?不想了解下她所在的城市,出生年月吗?好,请看下面分析

let reg2 = /^(\d{6})(\d{4})(\d{2})(\d{2})\d{2}(\d)(\d|X)$/
reg2.exec(str)

// 我们获取最后一位好像也没啥意义,那我们改改代码吧
let reg3 = /^(\d{6})(\d{4})(\d{2})(\d{2})\d{2}(\d)(?:\d|X)$/
reg3.exec(str)

    //通过数组的下标来拿有时候会出错吧,那我把需要用到的内容存储在一个分组中,并为每个元素赋上一个通俗易懂的名字是不是更好呢,那我们再改改代码
    let reg4 = /^(?<area>\d{6})(?<year>\d{4})(?<month>\d{2})(?<date>\d{2})\d{2}(?<sex>\d)(?:\d|X)$/
    reg4.exec(str)

4.2.2 . 获取字符串中的数字

涉及知识点 :

  1. 正则捕获的贪婪性
  2. 正则捕获的懒惰性
  3. 如何解决这两个特性的缺点
    let  str = "今年是 2020 年,我才 18 岁;我今天大概写了123456 6行代码";
    let reg1 =/\d+/;
    reg1.exec(str)

 // 上图中,我们是不是拿到了 2020 ,那我的 18 去拿了呢,不能因为我 谎报年龄就不给我捕获了吧,想想办法,再捕获一次
    reg1.exec(str)

 // 不够靓仔?,再想想办法
    let reg2 =/\d+/g; // 修饰符 g, 全局捕获
    reg2.exec(str)

    // 没有依旧啊,不行,我必须得证明我是 18 岁,我要再捕获一次
    reg2.exec(str)

// 终于证明了自己,彪子,放俩意大利炮庆祝下,~~~~~~~~
// 稍等,稍等,我今天写了 123456 这么多行代码吗?这么优秀吗?我数了数,只写了 6 行啊,那我该怎么体现我数代码行数没数错呢?

let  str = "我今天大概写了123456 6行代码";
let reg3 =/\d+?/g;
reg3.exec(str)

  • 总结上述问题:
  1. 在没有全局捕获的情况下,每次执行 exec,都是捕获的第一次出现的数字,index 也始终是第一次捕获数字的位置;在我们加了 g 全局修饰符后,再次执行,就能捕获到第二个数字 ,index 也发生了变化,所以当需要捕获的内容在字符串中多次出现时,我们无法一次捕获返回全部的值,需要执行多次捕获方法,这就是正则的懒惰性
  2. 默认情况下,正则捕获的时候,是按照当前正则所匹配的最长结果来获取的,而不是入我们数数般的一个个获取,这就是正则的贪婪性
 正则那么懒,我们可不应该这么懒啊,取其精华,弃其糟粕,我们高级 CV 工程师讲究的都是一把梭,上哪能搞一个一次捕获全局返回的方法多好,那么方法来了
RegExp.prototype.execAll = function(str = ""){
    // 正则没有加 g 全局匹配修饰符,则无论执行多少次捕获,结果始终是第一次捕获的内容
    if(!this.global) return this.exec(str)
    
    let execArr = [],
        res = this.exec(str);
    while(res){
        execArr.push(res[0])
        res = this.exec(str);
    }
    return execArr.length === 0 ? null : execArr
    
}

reg2.execAll(str)

这样我们就能一次性拿到字符串中所有的数字了

4.3 既然正则是用来处理字符串的,那字符串也为我们提供了捕获的方法

  1. match
  2. replace
  3. split

4.3.1 match 方法的使用

  • 字符串.match(正则) 返回符合匹配正则的字符串数组
   let  str = "今年是 2020 年,我才 18 岁";
   str.match(/\d+/g)

    我们使用 match 一次性的获取到了字符串中的所有数字,它是不是相当于给我执行了多个 exec 呢?,是不是和我们上面实现的 execAll 很相似呢? 所以上面所实现的 execAll 代码,基本就是 match 的底层实现原理了

4.3.2 replace 方法的使用

    //把时间字符串转换成我们想要的格式
    //我想把日期变成 2020年7月21日 10时36分38秒
    
    方法一 :
        1. 我先根据空格把 年月日和时分秒分开
        2. / 分割拿到 年月日
        3. : 分割拿到 时分秒
        4.开始拼接大法
        function formatTime(){
            let dateStr  = new Date().toLocaleString('chinese', { hour12: false }); // 2020/7/21 10:36:38
            let tempSplit = dateStr.split(' '); // ["2020/7/21", "10:36:38"]
            let tempSplit1 = tempSplit[0].split('/'); // ["2020", "7", "21"]
            let tempSplit2 = tempSplit[1].split(':'); //["10", "36", "38"]
            
            return `${tempSplit1[0]}${tempSplit1[1]}${tempSplit1[2]}${tempSplit2[0]}${tempSplit2[1]}${tempSplit2[2]}秒`
        }
        
    // 那如果我只想要年月日,只想要时分秒;只想要 日 时分秒呢? 我想要  2020-7-21 10-36分3-秒呢,脑壳疼不疼?,疼也要搞啊,咋搞?正则大法好
    
    方法二:咱给出默认格式,由用户指定想要的格式,想要啥给你返回啥
    
    String.prototype.formatTime = function(str = "{0}年{1}月{2}日 {3}时{4}分{5}秒"){
    
        // 拿到年月日时分秒 ["2020", "07", "21", "11", "08", "31"]
         let arr = this.match(/\d+/g).map(item =>{
            return item.length < 2 ? '0' + item : item
        })
        
        // 使用正则,拿到 {index} ,根据 index 拿到上 arr 中的内容进行替换
        return str.replace(/\{(\d+)\}/g,(content,index)=>{
            return arr[index] || '00'
        })
    }
    let dataString = new Date().toLocaleString('chinese', { hour12: false });
    dataString1 = dataString.formatTime('{2}日 {3}时{4}分');
    console.log(dataString1); // 21日 11时01分
    
    dataString2 = dataString.formatTime('{0}.{1}.{2} {3}.{4}.{5}');
    console.log(dataString2)

4.3.3 split 方法的使用

    let names = "Stephen Curry ;Klay Thompson ; Draymond Green ; Andrew Wiggins ";
    let re = /\s*(?:;|$)\s*/;
    let nameList = names.split(re);
    console.log(nameList); // ["Stephen Curry", "Klay Thompson", "Draymond Green", "Andrew Wiggins", ""]

5. | \b (?=) (?!) 等一些知识点的练习题

5.1 | 练习题

let reg = /12|34/   // 含 12 或 34
console.log(reg.test(1))    // false
console.log(reg.test(2))    // false
console.log(reg.test(3))    // false
console.log(reg.test(4))    // false
console.log(reg.test(12))   // true
console.log(reg.test(34))  // true
console.log(reg.test(124))  // true
console.log(reg.test(234))  // true
console.log(reg.test(1234))  // true

let reg = /1(2|3)4/ //含有 1 ,2 或者 3 ,4
console.log(reg.test(1))    // false
console.log(reg.test(2))    // false
console.log(reg.test(3))    // false
console.log(reg.test(4))    // false
console.log(reg.test(12))   // false
console.log(reg.test(34))  // false
console.log(reg.test(123))   // false
console.log(reg.test(124))  // true
console.log(reg.test(1234))  // false

let reg = /^12|34/  // 12 或者 34 开头
console.log(reg.test(1))    // false
console.log(reg.test(2))    // false
console.log(reg.test(3))    // false
console.log(reg.test(4))    // false
console.log(reg.test(12))   // true
console.log(reg.test(34))  // true
console.log(reg.test(123))   // true
console.log(reg.test(124))  // true
console.log(reg.test(1234))  // true

let reg = /^12|34$/  // 12 或者 34 开头, 12 或者 34 结尾
console.log(reg.test(1))    // false
console.log(reg.test(2))    // false
console.log(reg.test(3))    // false
console.log(reg.test(4))    // false
console.log(reg.test(12))   // true
console.log(reg.test(34))  // true
console.log(reg.test(123))   // true
console.log(reg.test(124))  // true
console.log(reg.test(1234))  // true

let reg = /^(12|34)$/  // 12 或者 34 
console.log(reg.test(1))    // false
console.log(reg.test(2))    // false
console.log(reg.test(3))    // false
console.log(reg.test(4))    // false
console.log(reg.test(12))   // true
console.log(reg.test(34))  // true
console.log(reg.test(123))   // false
console.log(reg.test(124))  // false
console.log(reg.test(1234))  // false


/*
*   () 的第三个作用: 分组引用 
*/

let str = '1001'
let reg = /^\d(\d)\1\d$/   // ()\1 代表引用一份前面的分容
reg.test(str)

5.2 \b 练习题

// 涉及知识点: \b ,处理边界问题
let str = "Complaining does not solve anything";
let reg = /\b([a-zA-Z])[a-zA-Z]*\b/g;
str = str.replace(reg,(item,first)=>{
    first=first.toUpperCase();
    item=item.substring(1);
    return first+item;
});
console.log(str) // "Complaining Does Not Solve Anything"

5.3 (?=)正向肯定预查:表示对后面边界的肯定匹配要求

let reg = /iPhone\s(?=X|XR|11)/i
let str1 = "iPhone X"
let str2 = "iPhone 2G"
str1.match(reg) // ["iPhone ", index: 0, input: "iPhone X", groups: undefined]
str2.match(reg) // null 

5.4 (?!)正向否定预查:表示对后面边界的否定匹配要求

let reg = /iPhone\s(?!X|XR|11)/i
let str1 = "iPhone X"
let str2 = "iPhone 2G"
str1.match(reg) // null 
str2.match(reg)  // ["iPhone ", index: 0, input: "iPhone 2G", groups: undefined]

5.5 正则中 test 的本意是用来匹配字符串是否符合规则,其实它也可以用来捕获,只是很少用

  • RegExp.$&:是获取当前大正则的内容
  • RegExp.$1~9:获取当前本次正则匹配后,第一个到第九个分组的信息
let str = '{1}A{2}B{3}C'
let reg = /\{(\d+)\}/g;
console.log(reg.test(str)) // true 
console.log(RegExp.$1) // 1

console.log(reg.test(str)) // true 
console.log(RegExp.$1) // 2

console.log(reg.test(str)) // true 
console.log(RegExp.$1) // 3

console.log(reg.test(str)) // false
console.log(RegExp.$1) // 3 // 存储的是上次捕获的内容

本文小伙只写了部分皮毛,希望各位 dalao 赐教,下次争取写点毛皮。