一篇文章带你玩转正则

505 阅读8分钟

正则表达式(RegExp)

正则就是用来制定规则的(只能处理字符串),会在开发中节省很多代码

  • 验证字符串是否符合某个规则(test)

  • 把字符串中符合规则的内容捕获(exec / match)

      let str = "good good study , day day up!",
          reg = /\d+/;
          
      reg.test(str); //=>false
      str = "2019-08-12";
      reg.exec(str); //=>["2019",index:0,inputs:"原始字符串"]
    

编写正则表达式

正则创建方式

根据不同的情况用不同的创建方式

1.字面量创建方式

    //=>两个斜杠之间包起来的,都是用来描述规则的元字符(不可添加变量)
    let reg1 = /\d+/;

2.构造函数模式创建

    //=>有两个参数:元字符字符串,修饰符字符串,中间添加变量
    let reg2 = new RegExp("\\d+"+ value +"g");

两种创建方式的区别

    //=>构造函数因为传递的是字符串,\需要写两个才代表斜杠
    let reg1 = /\d+/g;
        reg2 = new RegExp("\\d+","g");
  • 两个斜杠中间包起来的都是元字符

      //=>如果正则中要包含某个变量的值,则不能使用字面量方式创建
      let str = "zhengyi",
          reg = /^@"+str+"@$/; 
      reg.test("@zhengyi@");      //=>false
      reg.test('@"""strrr"@');    //=>true
    
  • 有需要变量的需求只能用构造函数创建

      =>它传递的规则是字符串,这样才能进行字符串拼接
      let str = "zhengyi",
          reg = new RegExp("^@"+str+"@$");
      reg.test("@zhengyi@");      //=>true
    

正则表达式由两部分组成

1、元字符

下面的元字符只是常用的,没有全部写出来

普通元字符:代表本身含义的

  • /zhengyi/ 此正则匹配的就是 "zhengyi"

量词元字符:设置出现的次数

  • *:零到多次
  • +:一到多次
  • ?:零次或者一次
  • {n}:出现n次
  • {n,}:出现n到多次
  • {n,m}:出现n到m次

特殊元字符:单个或者组合在一起代表特殊的含义

  • \ :转义字符(普通->特殊->普通)
  • . :除\n(换行符)以外的任意字符
  • ^ :以哪一个元字符作为开始
  • $ : 以哪一个元字符作为结束
  • \n : 换行符
  • \d : 0~9之间的一个数字
  • \D : 非0~9之间的一个数字 (大写和小写的意思是相反的,下面就不写大写的了)
  • \w : 数字、字母、下划线中的任意一个字符
  • \s : 一个空白字符(包含空格、制表符、换页符等)
  • \t : 一个制表符(一个TAB键:四个空格)
  • \b : 匹配一个单词的边界
  • x|y : x或者y中的一个字符
  • [xyz] : x或者y或者z中的一个字符
  • [^xy] : 除了x/y以外的任意字符
  • [a-z] : 指定a-z这个范围中的任意字符 =>[0-9a-zA-Z_]===\w
  • [^a-z] : 上一个的取反“非”
  • () : 正则中的分组符号
  • (?:) : 只匹配不捕获
  • (?=) : 正向预查
  • (?!) : 负向预查

2、修饰符

  • i (ignoreCase) : 忽略单词大小写匹配

  • m (multiline) : 可以进行多行匹配

  • g (global) : 全局匹配

      /Z/.test('z')  =>false
      /Z/i.test('z') =>true
    

元字符详解

1、^ $

    //=>以数字开头
    let reg = /^\d/;
    reg.test("zheng");      //=>false
    reg.test("1999zheng");  //=>true
    reg.test("zheng1999");  //=>false
    
    //=>以数字结尾
    let reg = /\d$/;
    reg.test("1999zheng");  //=>false
    reg.test("zheng1999");  //=>true

    //=>如果^/$两个都不加:字符串中包含符合规则的内容即可
    let reg1 = /\d+/;
    
    //=>如果^/$两个都加:字符串只能是和规则一致的内容
    let reg2 = /^\d+$/;
    
    //=>比如:简单的验证手机号码
    let reg = /^1\d{10}$/; =>意思是:以1开头 10个数字 并且结尾必须是数字

2、\

//=>下面的 . 不是小数点,是除\n外的任意字符
    let reg = /^2.3$/;
    reg.test("2.3");//=>true
    reg.test("2@3");//=>true
    reg.test("23");//=>false
    
    //=>基于转义字符 \ ,这里的 . 就是本意
    reg = /^2\.3$/;
    reg.test("2.3");//=>true
    reg.test("2@3");//=>false
    
    let str = "\\d",    //=>\需要写两个才代表斜杠
        reg = /^\d$/;   //=>\d代表0-9的数字
    reg.test(str); //=>false
    
    reg = /^\\d$/;  //=>把特殊符合转换为普通的
    reg.test(str); //=>true

3、x | y

    let reg = /^18|29$/;
    reg.test("18");     //=>true
    reg.test("29");     //=>true
    reg.test("129");    //=>true
    reg.test("189");    //=>true
    reg.test("1829");   //=>true
    reg.test("829");    //=>true
    reg.test("182");    //=>true
    //=>因为直接x|y会存在很乱的优先级问题,一般我们写的时候都伴随着小括号进行分组,因为小括号改变处理的优先级
    
    =>小括号:分组
    reg = /^(18|29)$/;  //=>只能是18或19
    reg.test("18");     //=>true
    reg.test("29");     //=>true
    reg.test("129");    //=>false
    reg.test("189");    //=>false

4、[]

  • 1.中括号中出现的字符一般都代表本身的含义

      let reg = /^[@+]$/;//=>@或+
      reg.test("@");     //=>true
      reg.test("+");     //=>true
      reg.test("@@");    //=>false
      reg.test("@+");    //=>false
      
      reg = /^[\d]$/;    //=>\d在中括号中还是0-9
      reg.test("d");     //=>false
      reg.test("9");     //=>true
    
  • 2.中括号中不存在多位数

     let reg = /^[18]$/; //1或8
     reg.test("1");      //=>true
     reg.test("8");      //=>true
     reg.test("18");     //=>false
     
     reg = /^[10-29]$/;  //=>1 或 0-2 或 9
     reg.test("1");      //=>true
     reg.test("9");      //=>true
     reg.test("0");      //=>true
     reg.test("2");      //=>true
     reg.test("10");     //=>false
    

正则的捕获

实现正则捕获的方法

  • 正则RegExp.prototype上的方法
    • exec
    • test
  • 字符串String.prototype上支持正则表达式处理的方法
    • replace (这个方法很重要)
    • match
    • splite
    • ......

基于 exec 捕获

  • 1、捕获的结果
    • null或者一个数组
    • 第一项:本次捕获到的内容
    • 其余项:对应小分组本次单独捕获的内容
    • index:当前捕获内容在字符串中的起始索引
    • input:原始字符串
  • 2、正则捕获的懒惰(默认只捕获第一个)

因为正则的懒惰型,每执行一次exec,只能捕获到一个符合正则规则的,在默认情况下, 执行n遍,获取的结果永远都是第一个匹配到的,其余的捕获不到

    let str = "zhengyi1999dao2019gong20nian",
        reg = /\d+/;
    reg.exec(str)); //=>["1999", index: 7, input: "zhengyi1999dao2019gong20nian"];
    
    //=>实现正则捕获的前提:当前正则要和字符串匹配,如果不匹配捕获的结果是null
    reg = /^\d+$/;
    reg.test(str); //=>false
    reg.exec(str); //=>null

基于 test 捕获(本意是匹配)

    //=>RegExp.$1~RegExp.$9:获取当前本次正则匹配后,第一个到第九个分组的信息
    let 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"
    
    console.log(reg.test(str));     //=>true
    console.log(RegExp.$1);         //=>"2"
    
    console.log(reg.test(str));     //=>false
    console.log(RegExp.$1);         //=>"2" 存储的是上次捕获的结果

replace 字符串中实现替换的方法 ( 伴随正则使用 )

    1.不用正则,replace 执行一次只能替换一个
    let str = "zhengyi@1999|zhengyi@|2019";
    //=>把"zhengyi"替换成"郑一"
    //=>执行一次
    str = str.replace("zhengyi","郑一") //=>"郑一@1999|zhengyi@|2019"
    str = str.replace("zhengyi","郑一").replace("zhengyi","郑一"); //=>//=>"郑一@1999|郑一@|2019"
    
    //2.用正则就可以全部替换,代码减少很多
    str = str.replace(/zhengyi/g,"郑一");

    //=>把"zhengyi"替换为"zhengyiyi"
    str = str.replace("zhengyi","zhengyiyi").replace("zhengyi","zhengyiyi");
        //=>"zhengyiyiyi@1999|zhengyi@|2019" 每次替换都是从字符串索引 0 开始找(类似于正则捕获的懒惰性)
    
    //=>基于正则g可以解决
    str = str.replace(/zhengyi/g,"zhengyiyi");  //=>"zhengyiyi@1999|zhengyiyi@|2019"

正则捕获的懒惰性

  • reg.lastIndex:当前正则下一次匹配的起始索引位置

  • 懒惰性捕获的原因:默认情况下lastIndex的值随之更改,每次都是从字符串索引 0 重新查找,找到的永远只是第一个

  • 解决办法:全局修饰符g

      let str = "zhengyi1999nian05yue14ri",
          reg = /\d+/;
      console.log(reg.lastIndex); //=>0 下面匹配捕获是从str的索引零的位置开始找
      console.log(reg.exec(str)); //=>["1999", index: 7, input: "zhengyi1999nian05yue14ri", groups: undefined]
      console.log(reg.lastIndex); //=>0 lastIndex没有改变,下次str还是从零开始查找匹配,找到的的永远是第一个匹配到的
    
      reg = /\d+/g;  //=>设置全局匹配符 g
      console.log(reg.exec(str)); //=>["1999",index: 7,...]
      console.log(reg.lastIndex); //=>11 设置全局匹配修饰符g后,第一次匹配完,lastIndex会自己修改
      
      console.log(reg.exec(str)); //=>["05",index: 15,...]
      console.log(reg.lastIndex); //=>17
      console.log(reg.exec(str)); //=>["14",index: 20,...]
      console.log(reg.lastIndex); //=>22
      console.log(reg.exec(str)); //=>null
      
      当全部捕获后,再次捕获的结果是null,lastIndex回归初始值零,再次捕获又从第一个开始
      console.log(reg.lastIndex); //=>0
      console.log(reg.exec(str)); //=>["1999",index: 7,...]
      
      //=>字符串符合正则规则,执行里面的代码
      if (reg.test(str)) {
          console.log(reg.lastIndex); //=>11 因为已经基于test匹配验证,lastIndex被修改为第一次匹配后的结果
          console.log(reg.exec(str)); //=>["05",index: 15,...]    从索引11开始匹配捕获
      }
    

正则的分组捕获

  • 第一项:大正则匹配的结果

  • 其余项:每一个小分组单独匹配捕获的结果

  • 如果设置了分组(改变优先级),但是捕获的时候不需要单独捕获,可以基于?:来处理

      //=>身份证号码
      let str = "13032119990514133X",
          reg = /^(\d{6})(\d{4})(\d{2})(\d{2})\d{2}(\d)(?:\d|X)$/;
      console.log(reg.exec(str));
      console.log(str.match(reg));
          //=>["13032119990514133X", "130321", "1999", "05", "14", "3", index: 0, input: "13032119990514133X"]
    
  • 分组引用

      //=>匹配这种重复字母的字符串"good"、"look"、"moon"、"foot"
      let reg = /^[a-zA-Z]([a-zA-Z])\1[a-zA-Z]$/; //=>分组引用就是通过 \数字(\1) 让其代表和对应分组出现一模一样的内容
      console.log(reg.test("book")); //=>true
      console.log(reg.test("deep")); //=>true
      console.log(reg.test("some")); //=>false
    

正则捕获的贪婪性

  • 正则捕获的贪婪性:默认情况下,正则捕获时,按照当前正则所匹配的最长结果获取

      let str = "zheng1999yi0514",
          reg = /\d+/g;
      console.log(str.match(reg)); //=>["1999","0514"]
    
  • 解决方法:在量词元字符后面设置 ? ,取消捕获时候的贪婪性(按照正则匹配的最短结果来获取)

      reg = /\d+?/g;
      console.log(str.match(reg)); //=>["1", "9", "9", "9", "0", "5", "1", "4"]
    

问号在正则中的五大作用

  • 问号左边是非量词元字符:本身代表量词元字符,出现零到一次
  • 问号左边是量词元字符:取消捕获时候的贪婪性
  • (?:) 只匹配不捕获
  • (?=) 正向预查
  • (?!) 负向预查

常用的正则表达式

1.验证是否为有效数字

    /*
     * 规则分析
     * 1.可能出现 + - 号,也可能不出现  [+-]?
     * 2.一位0-9都可以,多位首位不能是0 (\d|([1-9]\d+))
     * 3.小数部分可能有可能没有,一旦有后面必须有小数点+数字 (\.\d+)?
     */
    let reg = /^[+-]?(\d|([1-9]\d+))(\.\d+)?$/;

2.验证密码

    //=>数字、字母、下划线
    //=>6~16位
    let = reg = /^\w{6,16}$/;

3.验证真实姓名的

    /*
     * 1.汉字  /^[\u4E00-\u9FA5]$/
     * 2.名字长度 2~10位
     * 3.可能有译名 ·汉字  (·[\u4E00-\u9FA5]{2,10}){0,2}
     */
    let reg = /^[\u4E00-\u9FA5]{2,10}(·[\u4E00-\u9FA5]{2,10}){0,2}$/;

4.验证邮箱的

    let reg = /^\w+((-\w+)|(\.\w+))*@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/;
    
    //=> \w+((-\w+)|(\.\w+))*
    //1.开头是数字字母下划线(1到多位)
    //2.还可以是 -数字字母下划线 或者 .数字字母下划线,整体零到多次
    //=>邮箱的名字由“数字、字母、下划线、-、.”几部分组成,但是-/.不能连续出现也不能作为开始
    
    //=> @[A-Za-z0-9]+
    //1.@后面紧跟着:数字、字母 (1-多位)
    
    //=> ((\.|-)[A-Za-z0-9]+)*
    //1.对@后面名字的补充
    // 多域名     .com.cn
    // 企业邮箱    xxx@xxx-xxx-office.com
    
    //=> \.[A-Za-z0-9]+
    //1. 这个匹配的是最后的域名(.com/.cn/.org/.edu/.net...)

5.身份证号码

    /*
     * 1. 一共18位
     * 2. 最后一位可能是X
     *
     * 身份证前六位:省市县  130828
     * 中间八位:年月日
     * 最后四位:
     *   最后一位 => X或者数字
     *   倒数第二位 => 偶数 女  奇数 男
     *   其余的是经过算法算出来的
     */
    
    let reg = /^(\d{6})(\d{4})(\d{2})(\d{2})\d{2}(\d)(\d|X)$/;

正则使用的一些案例

1.把时间字符串进行处理

    let time = "2019-08-13";
    //=>变为"2019年08月13日"
    let reg = /^(\d{4})-(\d{1,2})-(\d{1,2})$/;
    
    //=>这样可以实现
    //time = time.replace(reg,"$1$2$3日");
    //console.log(time); //=>2019年08月13日
    
    //=>还可以这样处理 [str].replace([reg],[function])
    //1.首先拿REG和TIME进行匹配捕获,能匹配到几次就会把传递的函数执行几次(而且是匹配一次就执行一次)
    //2.不仅把方法执行,而且REPLACE还给方法传递了实参信息(和exec捕获的内容一致的信息:大正则匹配的内容,小分组匹配的信息....)
    //3.在函数中我们返回的是啥,就把当前大正则匹配的内容替换成啥
    /*
    time = time.replace(reg,(big,$1,$2,$3)=>{
        //=>这里的$1~$3是我们自己设置的变量
        console.log(big,$1,$2,$3);
    });
    */
    time = time.replace(reg,(...arg)=>{
        let [,$1,$2,$3]=arg;
        $2.length<2?$2="0"+$2:null;
        $3.length<2?$3="0"+$3:null;
        return $1+"年"+$2+"月"+$3+"日";
    });

2.单词首字母大写

    let str = "good good study,day day up!";
    let reg = /\b([a-zA-Z])[a-zA-Z]*\b/g;
    //=>函数被执行了六次,每一次都把正则匹配信息传递给函数
    //=>每一次ARG:["good","g"] ["good","g"] ["study","s"]...
    str = str.replace(reg,(...arg)=>{
        let [content,$1]=arg;
        $1=$1.toUpperCase();
        content=content.substring(1);
        return $1+content;
    });
    console.log(str); //=>"Good Good Study,Day Day Up!"

3.验证一个字符串中那个字母出现的次数最多,多少次?

    let str = "zhengyishiqianduanxiaobai",
        max = 0,
        res = [],
        flag = false;
    str = str.split('').sort((a, b) => a.localeCompare(b)).join('');
    for (let i = str.length; i > 0; i--) {
        let reg = new RegExp("([a-zA-Z])\\1{" + (i - 1) + "}", "g");
        str.replace(reg, (content, $1) => {
            res.push($1);
            max = i;
            flag = true;
        });
        if (flag) break;
    }
    console.log(`出现次数最多的字符:${res},出现了${max}次`);

4.formatTime 时间字符串的格式化处理

    function formatTime(templete = "{0}年{1}月{2}日 {3}时{4}分{5}秒") {
        let timeAry = this.match(/\d+/g);
        return templete.replace(/\{(\d+)\}/g, (...[, $1]) => {
            let time = timeAry[$1] || "00";
            return time.length < 2 ? "0" + time : time;
        });
    }

5.queryURLParams 获取URL地址问号和面的参数信息(可能也包含HASH值)

    function queryURLParams() {
        let obj = {};
        this.replace(/([^?=&#]+)=([^?=&#]+)/g, (...[, $1, $2]) => obj[$1] = $2);
        this.replace(/#([^?=&#]+)/g, (...[, $1]) => obj['HASH'] = $1);
        return obj;
    }

6.millimeter 实现大数字的千分符处理

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

最后

  • 有什么不正确的地方,欢迎评论指出