• JavaScript正则表达式入门


    正则表达式

    正则表达式本身是一种匹配模式,用计算机语言来描述我们需要匹配到的结构

    正则表达式语法

    正则表达式从匹配形式来说,要么匹配字符,要么匹配位置,以下从分别这两点展开学习

    匹配字符

    横向模糊匹配

    正则匹配到的字符串是不固定的 可以使用量词来指定片段出现的次数,次数会影响到字符串的长度,因为称之为横向模糊匹配

    示例

    var regex = /ab{2,5}c/g ;   //g 含有一个a,2-5个b,一个c的字符串
    
    var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
    console.log( string.match(regex) );
    
    // 匹配结果,只有满足前后位ac,中间只有2-5个b的字符串都匹配到了
    // => ["abbc", "abbbc", "abbbbc", "abbbbbc"]
    

    量词

    指定需要匹配的字符次数,一些常见的次数表示可以用等价的符号代替,如下图:

    纵向模糊匹配

    表示同一个位置可以匹配多种可能的字符

    示例

    var regex = /a[123]b/g;  // 匹配以a开头,以b结尾,中间含有123任意其中一个的字符
    
    var string = "a0b a1b a2b a3b a4b";
    console.log( string.match(regex) );
    
    // 匹配结果
    // => ["a1b", "a2b", "a3b"]
    

    字符组

    常见的纵向模糊匹配集合别名

    字符组

    字符匹配案例分析

    1.匹配日期

    匹配 2017-06-10

    分析

    年 四位数字即可 [0-9]{4}

    月,分为0开头和1开头 0[1-9] | 1[0-2]

    日,分为0、1、2、3开头 0[1-9] | [12][1-9] | 3[01]

    正则

    var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
    console.log( regex.test("2017-06-10") );
    // => true
    

    2.匹配id

    分析

    id="." 但是由于是贪婪匹配,就会匹配到最后一个双引号为止

    id=".*?" 可以使用惰性匹配来解决,但是效率低下

    id="[^"]*" 最佳

    正则

    var regex = /id="[^"]*"/
    var string = '<div id="container" class="main"></div>';
    console.log(string.match(regex)[0]);
    // => id="container"
    

    位置匹配

    位置在正则中表示相邻字符之间的位置,有以下描点

    ^

    表示字符串开头,多行字符表示行开头

    $ 结尾

    表示字符串开头,多行字符表示行结尾

    下面我们可以把字符串的开头和结尾用'#'代替

    单行

    var result = "hello".replace(/^|$/g, '#');
    console.log(result);
    
    // => "#hello#"
    

    多行

    var result = "I
    love
    javascript".replace(/^|$/gm, '#');
    console.log(result);
    
    /*
    #I#
    #love#
    #javascript#
    */
    

     单词边界

     是单词边界,具体就是 w 与 W 之间的位置,也包括 w 与 ^ 之间的位置,和 w 与 $ 之间的位置。

    var result = "[JS] Lesson_01.mp4".replace(//g, '#');
    console.log(result);
    // => "[#JS#] #Lesson_01#.#mp4#"
    

    B 非单词边界

    var result = "[JS] Lesson_01.mp4".replace(/B/g, '#');
    console.log(result);
    // => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
    

    (?=p) 先行断言

    比如 (?=l),表示 "l" 字符前面的位置,不包括p模式匹配的字符

    var result = "hello".replace(/(?=l)/g, '#');
    console.log(result);
    // => "he#l#lo"
    

    (?!p) 是 (?=p) 取反 先行否定断言

    除匹配p模式前面以外的位置

    var result = "hello".replace(/(?!l)/g, '#');
    console.log(result);
    // => "#h#ell#o#"
    

    (?<=p) 后行断言

    位置前面要满足匹配p模式,不包括p模式匹配的字符

    var result = "hello".replace(/(?<=l)/g, '#');
    console.log(result);
    // => "hel#l#o"
    

    (?<!p) 后行否定断言

    位置前面要满足匹配p模式,除此位置的其余位置

    var result = "hello".replace(/(?<=l)/g, '#');
    console.log(result);
    // => "#h#e#llo#"
    

    位置匹配案例分析

    1.数字千位分隔符表示法

    比如把 "12345678",变成 "12,345,678"。

    分析

    这个匹配一看就是匹配3位数字的前面的位置,可以使用先行断言来匹配 ?=d{3}+, 就可以做到

    正则

    var result = "12345678".replace(/(?=(d{3})+$)/g, ',')
    console.log(result);
    // => "12,345,678"
    
    // 这里尝试3的倍数位数字会发现开头也加上了,
    var result = "112345678".replace(/(?=(d{3})+$)/g, ',')
    console.log(result);
    // => ",112,345,678"
    
    // 限制此位置不能是开头即可
    var regex = /(?!^)(?=(d{3})+$)/g;
    result = "123456789".replace(regex, ',');
    console.log(result);
    // => "123,456,789"
    

    1.实现字符串trim方法

    分析

    trim是用来去除字符串首尾空白符,嗯,一读这句话,首尾,那不就是首尾
    ^ $嘛,至于空白符s就完事了

    正则

    function trim(str) {
      return str.replace(/^s+|s+$/g, '')
    }
    console.log(trim(' foobar '))
    

    trim方法是实现首尾去除空白符的,那么新推出的trimStart,trimEnd如何实现呢,嘻嘻,大家可以想想

    元字符转义问题

    转义这个问题是指一些符号在正则中拥有特殊的含义,比如表示字符串起始位置,那么如何表示这个字符串呢,那么就需要特殊的方法,成为转义,转义只需要在字符前加上

    正则表达式元字符

    ^、$、.、*、+、?、|、、/、(、)、[、]、{、}、=、!、:、- ,  
    

    匹配行为-贪婪匹配和惰性匹配

    这里的匹配行为由将我们正则转为计算机语言的状态机决定,常见的有DFA和NFA, 目前比较常见的是NFA,JavaScript也是采用NFA实现的正则引擎,NFA一个特点就是会产生回溯行为,生动地来将,它采用类似深度优先搜索思想,遍历可能匹配的字符串,一旦下一次匹配失效,即可回退到前一个状态,听起来很像拿着线球走出迷宫的故事,回溯行为从直观想就能得知会影响效率,在JavaScript正则中,常见的回溯形式为贪婪量词、惰性量词、分支结构,下面会依次介绍

    贪婪匹配

    最大范围匹配

    var regex = /d{2,5}/g;
    var string = "123 1234 12345 123456";
    console.log( string.match(regex) );
    // => ["123", "1234", "12345", "12345"]
    

    示例中会将数字尽可能匹配到,所以看到匹配的数字都是依照空白符分割开来的

    惰性匹配 ?

    最小匹配范围

    var regex = /d{2,5}?/g;
    var string = "123 1234 12345 123456";
    console.log( string.match(regex) );
    // => ["12", "12", "34", "12", "34", "12", "34", "56"]
    

    示例中,加了?的量词之后,正则会在匹配2个数字之后停止

    多选分支

    子模式任选其一 属于惰性匹配具体形式如下:(p1|p2|p3),其中 p1、p2 和 p3 是子模式,用 |(管道符)分隔,表示其中任何之一。

    var regex = /good|nice/g;
    var string = "good idea, nice try.";
    console.log( string.match(regex) );
    // => ["good", "nice"]
    

    括号

    在很多语言语法中,括号最常见的是代表优先级,我们看看括号在正则表达式中有什么特殊的用途呢?

    产生整体

    /ab+/ => a接上至少一个b
    如何把量词作用与一个整体 
    /(ab)+/
    

    分支结构

    表达分支的可能性

    表示p1或p2表达式任选其一
    var regex = /^I love (JavaScript|Regular Expression)$/;
    console.log( regex.test("I love JavaScript") );
    console.log( regex.test("I love Regular Expression") );
    

    分组引用

    用于捕获括号匹配的结果

    1.提取数据

    括号里的匹配字符串可以被直接引用,用以特定的场景

    提取年月日

    var regex = /(d{4})-(d{2})-(d{2})/g;
    var string = "2017-06-12";
    console.log( string.match(regex) );
    console.log( RegExp.$1 ); // 2017
    console.log( RegExp.$2 ); // 06
    console.log( RegExp.$3 ); // 12
    

    2.替换

    日期更换格式

    var regex = /(d{4})-(d{2})-(d{2})/;
    var string = "2017-06-12";
    var result = string.replace(regex, "$2/$3/$1");
    console.log(result);
    // => "06/12/2017"
    

    反向引用

    可以引用之前出现的分组结果

    有时候需要引用前面匹配的结果,比如说下面要求日期分隔符一致

    // 1表示出现的一个分组中的匹配结果
    var regex = /d{4}(-|/|.)d{2}1d{2}/;
    var string1 = "2017-06-12";
    var string2 = "2017/06/12";
    var string3 = "2017.06.12";
    var string4 = "2016-06/12";
    console.log( regex.test(string1) ); // true
    console.log( regex.test(string2) ); // true
    console.log( regex.test(string3) ); // true
    console.log( regex.test(string4) ); // false
    

    非捕获括号

    不捕获匹配的结果

    var regex = /(?:ab)+/g;
    var string = "ababa abbb ababab";
    console.log( string.match(regex) );
    // => ["abab", "ab", "ababab"]
    

    括号案例

    // 驼峰化
    function camelize (str) {
    return str.replace(/[-_s]+(.)?/g, function (match, c) {
    return c ? c.toUpperCase() : '';
    });
    }
    console.log( camelize('-moz-transform') );
    
    
    // 中划线化
    function dasherize (str) {
    return str.replace(/([A-Z])/g, '-$1').replace(/[-_s]+/g, '-').toLowerCase();
    }
    console.log( dasherize('MozTransform') );
    

    操作符优先级

    正则表达式可视化

    emmmm,虽然有优先级,但是有时候还是会看不懂,使用可视化工具帮助我们理解是一个很不错的idea,这里有一个正则可视化网址,在阅读不懂的正则表达式可以助你一臂之力。

    正则表达式修饰符

    修饰符 描述
    g 全局匹配,即找到所有的匹配
    y 全局匹配,即找到所有的匹配,但是此匹配要求每个子串是连续下标的
    i 忽略字母大小写
    m 多行匹配,使用^和$匹配多行时使用

    g

    默认是找到第一个匹配字符就停止,加了g修饰符就会找到字符串中所有匹配的字符,下面的例子一目了然

    var regex = /d/;
    var string = "123";
    console.log( string.match(regex) );  // [ '1', index: 0, input: '123' ]
    
    var regex = /d/g;
    var string = "123";
    console.log( string.match(regex) );  // [ '1', '2', '3' ]
    

    y

    与g行为一致的是,找到全局匹配的子串,但是y有特殊行为,要求每一个匹配的子串起始位置必须是上一个子串的结束位置,看一下下面的例子

    var s = 'aaa_aa_a';
    var r1 = /a+/g;
    var r2 = /a+/y;
    
    r1.exec(s) // ["aaa"]
    r2.exec(s) // ["aaa"]
    
    r1.exec(s) // ["aa"]
    r2.exec(s) // null
    

    i

    i的含义比较简单

    var regex = /A/i;
    var string = "a";
    console.log( string.match(regex) ); // [ 'a', index: 0, input: 'a' ]
    
    var regex = /A/i;
    var string = "A";
    console.log( string.match(regex) ); // [ 'A', index: 0, input: 'A' ]
    

    m

    m就是为了让^和$变成行头和行尾

    var regex = /^A$/g;
    var string = "A
    A";
    console.log( string.match(regex) ); // null
    
    var regex = /^A$/mg;
    var string = "A
    A";
    console.log( string.match(regex) ); // [ 'A', 'A' ]
    
    

    正则表达式编程

    在我们使用正则表达式匹配之后,JavaScript提供了一些操作给我们使用,下面依次介绍

    起始API exec

    exec是正则表达式编程中最基本的API,它拥有对字符串匹配,迭代的能力,后续API可以理解为特定场景的exec封装的API,在默认情况下返回第一次匹配的字符串,g修饰符下,每次从上一个匹配结果末端下标开始查找下一个匹配结果

    /**
    方法返回正则匹配的字符串
    * @param string 执行的字符串
    * @return {RegExpExecArray | null} 正则执行数组或者null
    */
    exec(string: string): RegExpExecArray | null;
    
    let reg = /d/g;
    let s = "123456"
    console.log(reg.exec(s)); // [ '1', index: 0, input: '123456' ]
    console.log(reg.exec(s)); // [ '2', index: 1, input: '123456' ]
    

    验证

    检测目标字符串是否有满足匹配的子串,注意验证是有没有子串,比较常用的是test,它直接返回boolean表示验证结果

     /**
      方法返回一个布尔值表示是否在给定字符串中,存在一个匹配正则的字符串
      * @param string 被测试的字符串
      * @return {boolean} 是否匹配存在匹配子串
    */
    test(string: string): boolean;
    

    可以理解成一下代码

    // 若有一个或多个匹配,第一次匹配都能匹配到结果,否则返回null
    RegExp.prototype.test = function (str) {
      return !!this.exec(str)
    }
    
    let reg = /d/;
    let s = "123456"
    console.log(reg.test(s)); // true
    

    实例

    var regex = /d/;
    var string = "abc123";
    console.log( regex.test(string) ); // true
    

    切分

    在匹配标志符位置进行切分

    /**
      方法按照给定的正则返回分割后的子串数组
      * @param separator 分割器,可以使一个字符串或者一个正则表达式 
      * @param limit 返回结果数组的长度
      * @return {string []} 分割后的字符串数组
      */
    split(separator: string | RegExp, limit?: number): string[];
    

    可以理解成以下代码

    String.prototype.split = function(reg, limit) {
      let curIndex = -1
      // 此时this为String对象,需要拆包
      let str =  this.valueOf()
      let splitArr = []
      // 执行exec方法
      while(result = reg.exec(str)) {
        let findIndex = result.index 
        // 分隔符相邻
        const isadjoin =  curIndex + 1 === findIndex
        // 分隔符之间的字符
        let splitMiddleStr = str.substring(curIndex + 1, findIndex)
        // 此次分割出来的字符
        let splitedStr = isadjoin ? "" :  splitMiddleStr
        splitArr.push(splitedStr)
        curIndex = findIndex
      }
      return splitArr.slice(limit)
    }
    

    实例

    var regex = /,/;
    var string = "html,css,javascript";
    console.log( string.split(regex) );
    // => ["html", "css", "javascript"]
    
    var regex = /,/;
    var string = "html,css,javascript";
    console.log( string.split(regex, 1) );
    // => ["html"]
    

    注意点

    1.使用正则切分,若正则含有捕获括号, 结果会带有正则匹配部分

    var string = "html,css,javascript";
    console.log( string.split(/(,)/) );
    // =>["html", ",", "css", ",", "javascript"]
    

    提取

    匹配后提取部分数据,比较常用的是match

    /**
    * 方法会以给定的正则去匹配字符串,若匹配成功,则返回匹配数组,否则返回null
    * @param regexp 字符串或者正则对象
    * @return {RegExpMatchArray | null} 匹配结果数组或者null
    */
    match(regexp: string | RegExp): RegExpMatchArray | null;
    

    可以理解成以下代码

    var string = "2017.06.27";
    var regex1 = /(d+)/;
    var regex2 = /(d+)/g;
    
    String.prototype.match = function (reg) {
      let str = this.valueOf()
      // 是否含有g修饰符
      let isGlobal = reg.global
      let result = []
      let curString = ""
      if(isGlobal) {
        // 返回全部匹配字符串数组
        while(curString = reg.exec(str)) {
          result.push(curString[1])
        }
      }else {
        // 返回第一次匹配的字符串数组
        result = reg.exec(str)
      }
      return result
    }
    console.log(string.match(regex1)); // [ '2017', '2017', index: 0, input: '2017.06.27' ]
    console.log(string.match(regex2)); // [ '2017', '06', '27' ]
    

    实例

    
    var regex = /^(d{4})D(d{2})D(d{2})$/;
    var string = "2017-06-26";
    console.log( string.match(regex) );
    // =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]
    
    

    注意点

    1.match会把第一个参数的字符串转成正则

    var string = "2017.06.27";
    console.log( string.match(".") );
    // => ["2", index: 0, input: "2017.06.27"]
    //需要修改成下列形式之一
    console.log( string.match("\.") );
    console.log( string.match(/./) );
    // => [".", index: 4, input: "2017.06.27"]
    // => [".", index: 4, input: "2017.06.27"]
    

    2.match返回值

    var string = "2017.06.27";
    var regex1 = /(d+)/;
    var regex2 = /(d+)/g;
    console.log( string.match(regex1) );
    // => ["2017", "2017", index: 0, input: "2017.06.27"]  不带g返回第一个匹配的字符串,分组捕获的内容,整体匹配的第一个下标,输入的目标字符串
    
    console.log( string.match(regex2) );  // 带g返回所有匹配的内容
    // => ["2017", "06", "27"]
    
    

    替换

    替换匹配的信息进行处理,使用replace

    /**
    * 方法按照匹配规则, 使用替换值,将匹配字符串替换为替换值
    * @param searchValue 需要匹配的字符串或者正则
    * @param replaceValue 替换字符串
    * @return {string} 替换后的字符串
    */
    replace(searchValue: string | RegExp, replaceValue: string): string;
    
    /**
    * 方法按照匹配规则, 
    * @param searchValue 需要匹配的字符串或者正则
    * @param replacer 替换器
    * @return {string} 替换后的字符串
    */
    replace(searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string;
    

    可以理解成以下代码,这里仅实现全局替换,让大家了解replace是一个特定场景下的api即可

    var string = "2017.06.27";
    var regex2 = /d+/g;
    
    String.prototype.replace = function (reg, replaceValue) {
      // 这里this是String对象,使用valueOf取出字符串值
      let str = this.valueOf()
      // 先用正则分割字符串
      let strArr = str.split(reg)
      // 用replaceValue来填充替换的地方
      str = strArr.join(replaceValue)
      return str
    }
    console.log(string.replace(regex2, "1")); // 1.1.1
    

    实例

    var string = "2017-06-26";
    var today = new Date( string.replace(/-/g, "/") );
    console.log( today );
    // => Mon Jun 26 2017 00:00:00 GMT+0800 (中国标准时间)
    

    注意点

    1.当第二个参数是字符串是,有以下特殊字符

    2.当第二个参数是函数时,函数传入参数

    // 从左往右分别是匹配的字符串,捕获组,匹配字符串的起始位置,输入字符串
    [match, $1, $2, index, input]
    
    let  a = "1234 2345 3456".replace(/(d)d{2}(d)/g, function (match, $1, $2, index, input) {
      console.log([match, $1, $2, index, input]);
    });
    // => ["1234", "1", "4", 0, "1234 2345 3456"]
    // => ["2345", "2", "5", 5, "1234 2345 3456"]
    // => ["3456", "3", "6", 10, "1234 2345 3456"]
    

    3.replace可以嵌套使用

    在一些需求里,我们无可避免需要多次正则处理,比如先找到一个整体,再替换这个整体里面的一部分,以缩小影响范围,可以先用replace匹配一次子串,然后再次缩小范围匹配

    let domStr = `<div style="font-family: Times, serif, 'Times New Roman'" class="333">
        <div style="font-family: Verdana, Geneva, Tahoma, sans-serif;color: #333">
          <div style="font-family: Verdana, Geneva, Tahoma, sans-serif;color: #333"></div>
        </div>
    </div>`
    // 匹配style="" 关键是中间部分
    let styleRegex = /style="[^"]*"/g  
    let result = domStr.replace(styleRegex, function(style) {
      console.log(style);
       let isoffFontFamily = /font-family:([^;])*(Times New Roman)+([^;"])*/;
       return style.replace(isoffFontFamily, "");
    })
    
    console.log(result);
    // <div style="" class="333">
    //     <div style="font-family: Verdana, Geneva, Tahoma, sans-serif;color: #333">
    //       <div style="font-family: Verdana, Geneva, Tahoma, sans-serif;color: #333"></div>
    //     </div>
    // </div>
    

    构造函数与实例

    1.一般不使用构造函数生成正则表达式,优先使用字面量,除非需要动态生成正则表达式

    2.修饰符都有其对应的对象布尔属性表面是否启用

    修饰符 实例属性
    g global
    y sticky
    i ingnoreCase
    m multiline

    3.可以用过source实例属性来查看动态构建的正则表达式结果

    var className = "high";
    var regex = new RegExp("(^|\s)" + className + "(\s|$)");
    console.log( regex.source )
    // => (^|s)high(s|$) 即字符串"(^|\s)high(\s|$)"
    

    正则表达式的构建

    如何针对问题,构建一个合适的正则表达式

    法则

    1.是否有必要使用正则

    人在学习一个新东西之后,很容易陷入到无所不用,正则亦是如此,其实很多时候,我们能够用字符串API解决,就不需要正则出手,以下是一些例子

    var string = "2017-07-01";
    var regex = /^(d{4})-(d{2})-(d{2})/;
    console.log( string.match(regex) );
    // => ["2017-07-01", "2017", "07", "01", index: 0, input: "2017-07-01"]
    
    var result = string.split("-");
    console.log( result );
    // => ["2017", "07", "01"]
    

    在年月日的例子中,我们使用了正则来获取年月日,相比采用分隔符,增加了代码的复杂性。

    2.是否有必要严格匹配

    一般来说,正则由于其复杂性,复杂度上来,需要严格匹配就很困难,应该结合场景,够用就行,或者可以做一些预处理字符串,使得匹配难度降低。

    3.效率

    1.尽量使用具体字符组代替通配符,减少回溯

    /.*/ 虽然可以匹配任意字符,但是因为贪婪性质,容易出现回溯行为,比如下面的
    匹配"abc" 使用/".*"/
    // 123"abc"456 
    

    在匹配过程中进行了4次回溯,由于回溯需要保存之前的状态,所以需要额外的空间,另外,回溯行为很直观地可以看出来影响效率,/"[^"]*"/,就可以避免回溯行为

    2.独立出确定字符,加快匹配判断速度

    /a+/ 
    // 如果能确定是比如字符a是存在的,可以改写成一下正则,加快匹配判断速度
    /aa+/
    

    3.对多选分支提取公共部分,减少匹配过程中可消除的重复

    /^abc|^def/ //修改成 
    /^(?:abc|def)/。
    // 多选分支是惰性的,在不匹配分支的时候,会尝试其他分支,公共的部分也会进行这个匹配,然而公共的部分没有必要多次匹配,可以提取出来,减少匹配中的重复过程
    

    4.使用非捕获括号

    在我们不需要捕获括号中的内容时,可以使用非捕获括号来省掉原本用户保存捕获内容的内存

    实践

    1.快速找出Vue源码中的所有正则

    尝试一

    //.*//
    
    // 这会把注释、路径、域名都匹配进去
    
    // /* */  
    

    结果

    尝试2

    //(S)+//
    
    // 这里匹配了非空字符串,而且必须至少出现一次
    
    // /* */  
    

    结果

    一下子少了很多,但是仍然会匹配到不是正则的 /ggg/ 可能只是路径中的一部分,但是我们需要检查的量已经可以接受了。

  • 相关阅读:
    打印杨辉三角
    插值排序
    各种冒泡排序法
    Linux系统命令符01
    2.1博客系统 |基于form组件和Ajax实现注册登录
    python面试笔试题,你都会了吗?快来复习
    1.2博客系统 |登录页| 验证码
    1.1博客系统| 表结构
    第五章:5.2面向对象-绑定方法和非绑定方法| 内置方法 |元类
    11.Django|中间件
  • 原文地址:https://www.cnblogs.com/sefaultment/p/11374103.html
Copyright © 2020-2023  润新知