学前端:正则表达式快速入门

262 阅读3分钟

正则(regular expression : RegExp)

用来处理字符串的规则
主要用于: 验证(test)和捕获(exec,match…)

2种创建方式:

1. let reg = new RegExp('\d+') 可传2参数,元字符 和 修饰符
    
2. let reg = /\d+/

`注意字符串里的‘\’ 会被进行处理 要进行转移才能得到如下的斜杠 即‘\’`

主要组成:元字符,修饰符

  • 元字符
    • 1.量词
      * 0到多次 = {0,}
      + 1到多次 = {1.}
      ? 0到1次 = {0,1}
      {n} 出现n次
      {n,} 出现n到多次
      {n,m} 出现n到m次
    • 2.特殊字符
      \ 转义字符 (普通《=》特殊)
      . 除了\n(换行)以外的 任意字符 => [^\n]
      ^ 以某字符开头
      $ 以某字符结尾
      \n 换行符
      \d 0~9之间的数字 =>[0-9] , 0|1|2|3|4|5|6|7|8|9
      \D 不是0~9间的数字 =>[^0-9]
      \w 单字符,数字,字母,下划线中的任一字符 => [0-9a-zA-Z_]
      \W 非单字符,不是数字,字母,下划线中的任一字符 => [^0-9a-zA-Z_]
      \s 一个空白字符(空格,制表,换行)=>[\t\n ]
      \t 制表符(一个TAB键:4个空格)
      \b 匹配一个单词的边界
      x|y x,y中的某一个
      [xyz] x,y,z中的某一个
      [^xy] 除了x,y的任意字符
      [a-z] a-z中的任意字符
      [^a-z] 不是a-z中的字符
      () 正则分组
      (?:) 只匹配不捕获
      (?=) 正向预查
      (?!) 负向预查
    • 3.普通元字符
      /i have a dream/ 正常匹配,包含‘i have a dream’即可
  • 修饰符(IMG)
    • i =>ignoreCase 忽略单词大小写匹配
      m =>multiline 可以进行多行匹配
      g =>global 全局匹配
    • /A/.test('lalala') =>false
      /A/i.test('lalala') =>true

示例:

  ^ / $:
  
    => ^/$两个都不加:字符串中包含符合规则的内容即可
       let reg1 = /\d+/;  包含[0-9]即可

    => ^/$两个都加:字符串只能是和规则一致的内容
       let reg2 = /^\d+$/;  每位都必须是[0-9]

    => 验证手机号码(11位,第一个数字是1即可)
       let reg = /^1\d{10}$/;    


  \ :

    `有时候需要转义,把特殊含义与普通含义来回转换,要能人脑识别出特殊含义`

      => .不是小数点,是除\n外的任意字符
         let reg = /^2.3$/;  23中间可以是任意字符
             reg = /^2\.3$/; 只能是2.3

      => 匹配(\d),代表0-9的数字
         reg = /^\d$/; 

      => 匹配字符串\d,也就是“\\d”
         reg = /^\\d$/; 

  | :
     `或这个符号会衍生出很多复杂规则,必要时配合 ()使用`

     let reg = /^18|29$/;
     符合的匹配规则:
      1.2位) 18开头,或29结尾
      2.3位) 1开头,9结尾,中间28

     reg = /^(18|29)$/;
     只能是18或者29      

  [] :
     `中括号中的字符一般都是原意,且里面没有多位数,对外也是代表一位字符`

    let reg = /^[@+]$/;
    代表只能是字符 @/+ 出现一次 ,量词失效

    reg = /^[\d]$/; 
    代表0-9 出现一次,\d在中括号中还是0-9(特殊字符生效了)

    reg = /^[18]$/;
    1/8 出现一次

    reg = /^[10-29]$/; 
    1或者0-2或者9,出现一次

正则应用:

1.验证有效数字

  规则:
    1. 开头可以是 +,-号,也可以没有             [+-]?

    2. 一位数 多位数(0不能打头)                (\d | [1-9]\d+)

    3. 小数部分可有可无,但是点后面至少一位数字     (.\d+)?

  `let reg = /^[+-]?(\d|([1-9]\d+))(.\d+)?$/`

2.验证密码

  规则:
    1. 616位数 ,字母 数字 下划线

  `let reg = /^\w{6,16}$/`

3.验证姓名

  规则:
    1.汉字范围 '\u4E00' - '\u9FA5'

    2.一般长度 2~10

    3.可能有多段 xx·xx·xx  

      ‘列奥纳多·达·芬奇’ ‘弗拉基米尔·弗拉基米罗维奇·普京’

`reg = /^[\u4E00-\u9FA5]{2,10}(·[\u4E00-\u9FA5]{1,10})*$/   `



4.验证邮箱

  规则:(以@为分割)
    1.开头是 字母数字下划线 0到多位数

    2.之后可以出现 .\w+或者-\w+ 0到多次

    3. 邮箱名字也就是@之前,可以出现 字母 数字 下划线 · -(后2个不落单)

    4. @紧后面是 字母或数字 出现1到多次

    5.4的补充, .或-后面跟字母数字 出现0到多次 

    6. 最后的域名,.[a-zA-Z0-9]+
    
eg: xyz-123.self@zhufeng-peixun-office.com

`reg = /^\w+([-.]\w+)*@[a-zA-Z0-9]+((.|-)[a-zA-Z0-9]+)*.[a-zA-Z0-9]+$/`



5.身份证号

  规则:
    1.一共18位 ,最后一位可能是 x

    2. 身份证前六位:省市县  211481

       中间八位:年月日      19981023

       最后四位:           0213

           最后一位 => X或者数字

           倒数第二位 => 偶数 女  奇数 男

           其余的是经过算法算出来的

`reg = /^(\d{6})(\d{4})(\d{2})(\d{2})\d{2}(\d)([\dX])$/`

正则两种常见方式的区别:

  • 1.let reg = /\d+/g

    • 字面量创建,斜杠中的当作元字符处理
      无法拼接外界变量进来,因为变量名都被解析为元字符
  • 2.reg = new RegExp("\d+","g")

    • 传递字符串会被先解析一次,\需要写两个才代表斜杠

new RegExp("^@"+type+"@$"); 可以拼接变量

reg = /^@"+type+"@$/; 无法拼接type进来

正则捕获相关方法:

  • 正则RegExp.prototype上的方法

    • exec
    • test
  • 字符串String.prototype上支持正则表达式处理的方法

    • replace
    • match
    • splite
    • …….

应用举例:

`let str = "zhufeng2019yangfan2020qihang2021";`

`let reg = /\d+/g;`

基于exec实现正则捕获:

 1.得到的结果为 null 或者一个数组
 
 2.数组第一项为匹配捕获到的整个内容, 第二项往后为小分组捕获到的内容
   index:匹配到的内容在整个输入中的索引位置(全匹上就是0)
   input: 原始字符串

 3.每次执行exec 只能捕获到一个符合规则的,
   但是不加g情况下(globalfalse),执行多次也只捕获第一个,就算修改lastIndex也没用。
   正则捕获的懒惰性:默认只捕获第一个

懒惰性解决方案:

1.默认情况下(不加g),lastIndex代表当前正则下一次匹配的
  起始索引位置,不会自动修改,手动修改了也无效,每次都从0开始。

2.加全局修饰符g即可,开启后lastIndex会自动改变,
  不管是 exec,test,手动修改 ,都会生效影响下次检索起始位置

3.开启g后,exec或者test ,等到执行结果为 null/false,会把lastIndex的值归零。

给正则手写一个execAll方法,一次性捕获所有匹配项:
    function execAll(str){
      let arr=[]
      if(!this.global) return this.exec(str)
      while(res=this.exec(str)){
        arr.push(res[0])
      }
      return arr.length>0?arr:null;
    }
字符串的match方法 可以实现同样的功能
str.match(reg)


分组捕获

let str = "130828199012040112";
let reg = /^(\d{6})(\d{4})(\d{2})(\d{2})\d{2}(\d)(?:\d|X)$/;

console.log(str.match(reg));
这仅仅是一次的匹配结果,因为没加g,所以结果同exec
["130828199012040112", "130828", "1990", "12","04", "1", index: 0, input: "130828199012040112"]

第一项:大正则匹配的结果
其余项:()包起,捕获的小分组
`如果在正则里用()来改变优先级,又不想分组捕获,用?:可以取消`


str = "{0}年{1}月{2}日";
reg = /\{(\d+)\}/g;

如果想在多次捕获的情况下,同时也得到小分组 match是做不到的(一旦开启了g修饰符,结果就只包含捕获的整体)

str.match(reg) =>["{0}", "{1}", "{2}"]

手写扩展方法:
    let arrOuter=[],arrInner=[];

      while(res=reg.exec(str)){
        arrOuter.push(res[0])
        arrInner.push(res[1])
    }
    console.log(arrOuter,arrInner)


分组第三作用:分组引用 (1.提升正则内优先级 2.分组捕获 3.分组引用)

  let str = "book"; 类似的有"good""look""moon""foot"...
  let reg = /^[a-zA-Z]([a-zA-Z])\1[a-zA-Z]$/;

  \1 表示和第一个分组实际捕获的内容相同
  分组引用就是通过“\数字”让其代表和对应分组出现一模一样的内容

正则的捕获贪婪性:

  str = "你好2019@2020世界";
  reg = /\d+/g;
  console.log(str.match(reg)); 结果:["2019","2020"]

  默认情况下,正则尽可能捕获最长的
  可用量词后加?取消贪婪性
  reg = /\d+?/g;
  console.log(str.match(reg));
  结果: ["2", "0", "1", "9", "2", "0", "2", "0"]


?的用法总结:

  量词:元字符?,元字符出现01次

  取消贪婪:量词?,取消捕获时贪婪性

  只匹配不捕获: (?:分组内容)

  正向预查:?=

  负向预查:?!    

其他的正则捕获

1. 依靠test,在RegExp上拿到分组信息

  str = "{0}年{1}月{2}日";
  reg = /{(\d+)}/g;

  console.log(reg.test(str));   true 
  console.log(RegExp.$1);       "0"
  console.log(reg.test(str));   true 
  console.log(RegExp.$1);       "1"

  RegExp.$1~RegExp.$9:获取正则实例匹配后,第一个到第九个分组的信息,
  每次(exec,test)捕获到后都会覆盖之前的分组。



2. 字符串的replace方法,不但可以多次匹配,还能拿到分组(完胜 exec,match)

  1.第一个参数可以是正则,也可以是字符串,表示要被替换的项。
    但是只有是正则且加g的情况下,才会自动全局匹配
  2.第二个参数,如果是函数的话,每次匹配到就会执行一次该函数,函数拿到
    的参数和exec的结果一致(匹配结果及分组)并用返回值替换匹配项。


  用法示例:

  1.日期字符串格式化
    time = "2019-08-13";  变为"2019年08月13日"
    reg = /^(\d{4})-(\d{1,2})-(\d{1,2})$/;
    `只匹配一次,里面有三个分组`
    
    time= time.replace(reg,(...[,$1,$2,$3])=>{
      return `${$1}${$2}${$3}日`
    })


  2.首字母大写
    str = "good good study,day day up!";
    //reg =/\b([a-zA-Z])[a-zA-Z]\b$/g
    reg =/([a-zA-Z])(?:[a-zA-Z])*[a-zA-Z]/g

    函数执行6次
    str=str.replace(reg,(...[word,letter])=>{
      return letter.toUpperCase()+word.substr(1)
    })


  3.验证字符串出现次数
    str ='sdkjfsdkjhfssssssdasajdlkjlkpoikkkkkkkkkejowhojdfds'
    max=0,res=null
    map={}

    while(len=str.length){
      var letter=str[0]
      str=str.replace(new RegExp('('+letter+')\1*','g'),'')
      var num =len-str.length;
      map[num]?map[num].push(letter):map[num]=[letter];
      if(num>max){
        max=num
        res=letter
      }
    }
    console.log(max+':',res,map[max])


  4. formatTime 、 queryUrl、 thousandSign封装成库

    a.日期字符串模板格式化(放在字符串原型上)
    
        function formatTime(template='$0年$1月$2日 $3时$4分$5秒'){
          let arr=this.match(/\d+/g)
          return template.replace(/$(\d+)/g,(...[,index])=>{
            let item=arr[index] || '00'
            return item.length>=2?item:'0'+item
          })
        }



    b.url参数拆分

    function queryUrl(){
      let regData=/([^?#=&]+)=([^?#=&]+)/g
          regHash=/#([^?=&]+)/g
      let res={}
      this.replace(regData,(...[,$1,$2])=>{
        res[$1]=decodeURIComponent($2)
      })
      this.replace(regHash,(...[,$1])=>{
        res.hash=$1        
      })
      return res
    }



    c.千分符

    function thousandSign(){
        let reg=/\d{1,3}(?=(\d{3})+$)/g
        return this.replace(reg,(content)=>{
          return content+','
        })
    }


    // 批量挂载写好的方法到String原型上
    ['formatTime','queryUrl','thousandSign'].forEach(
    (item)=>{
        String.prototype[item] = eval(item)
    })