美文网首页
第六届360前端星计划_正则的三个应用场景

第六届360前端星计划_正则的三个应用场景

作者: lhang_91ad | 来源:发表于2020-04-10 13:19 被阅读0次

    正则的三个应用场景

    主讲人

    • 王峰
    • 360前端技术专家
    • 奇舞团
    1. 正则表达式的创建和使用

    2. 三个应用场景

      1. 正则与数值

      2. 正则与颜色

      3. 正则与URL

    3. 正则表达式的创建和使用

      正则表达式的表示:

      1. 使用正则表达式字面量

        const reg = /[a-z]\d+[a-z]/i;
        

        优点

        • 简单方便

        • 不需要考虑二次转义

        缺点

        • 子内容无法重复使用

        • 过长的正则导致可读性差

      2. 使用 RegExp 构造函数

        const alphabet = '[a-z]';
        const reg = new RegExp(`${alphabet}\\d+${alphabet}`, 'i');
        

        优点

        • 子内容可以重复使用

        • 可以通过控制子内容的粒度提高可读性

        缺点

        • 二次转义的问题非常容易导致 bug
        const reg = new RegExp(`\d+`);
        reg.test('1'); // false
        reg.test('ddd'); // true
        

      正则表达式的常见用法

      1. RegExp.prototype.test()

        const reg = /[a-z]\d+[a-z]/i;
        ​
        reg.test('a1a'); // true
        reg.test('1a1'); // false
        reg.test(Symbol('a1a')); // TypeError
        

        输入

        输出

        true 或者 false,表示匹配成功或失败

      2. RegExp.prototype.source 和 RegExp.prototype.flags

        const reg = /[a-z]\d+[a-z]/ig;
        ​
        reg.source; // "[a-z]\d+[a-z]"
        reg.flags; // "gi"
        

        get RegExp.prototype.source

        返回当前正则表达式的模式文本的字符串

        get RegExp.prototype.flags

        es2015新增,返回当前正则表达式的修饰符的字符串,会对修饰符按照字母升序进行排序(gimsuy)

      3. RegExp.prototype.exec() 和 String.prototype.match()

        const reg = /[a-z]\d+[a-z]/i;
        ​
        reg.exec('a1a'); // ["a1a", index: 0, input: "a1a", groups: undefined]
        reg.exec('1a1'); // null
        'a1a'.match(reg); // ["a1a", index: 0, input: "a1a", groups: undefined]
        '1a1'.match(reg); // null
        

        输入

        RegExp.prototype.exec 要求输入字符串,遇到非字符串类型会尝试转换

        String.prototype.match 要求输入正则表达式,遇到其它类型会先尝试转成字符串,再以字符串为 source 创建正则表达式

        输出

        匹配成功,返回匹配结果

        匹配失败,返回 null

        const reg = /(a)/g;
        ​
        reg.exec('a1a'); // ["a", "a", index: 0, input: "a1a", groups: undefined]
        'a1a'.match(reg); // ["a", "a"]
        

        两个的不同

        当正则表达式含有 g 修饰符时,RegExp.prototype.exec 每次只返回一个匹配结果,数据格式和不含 g 修饰符相同。

        String.prototype.match 会返回所有的匹配结果,数据格式会变为字符串数组。

        由于 String.prototype.match 返回的数据格式不固定,因此大多数情况都建议使用 RegExp.prototype.exec

      4. RegExp.prototype.lastIndex

        const reg = /(a)/g;
        const str = 'a1a';
        ​
        reg.lastIndex; // 0
        reg.exec('a1a'); // ["a", "a", index: 0, input: "a1a", groups: undefined]
        reg.lastIndex; // 1
        reg.exec('a1a'); // ["a", "a", index: 2, input: "a1a", groups: undefined]
        reg.lastIndex; // 3
        reg.exec('a1a'); // null
        reg.lastIndex; // 0
        

        当前正则表达式最后一次匹配成功的结束位置(也就是下一次匹配的开始位置)

        注意:lastIndex 不会自己重置,只有当上一次匹配失败才会重置为 0 ,因此,当你需要反复使用同一个正则表达式的时候,请在每次匹配新的字符串之前重置 lastIndex!

      5. String.prototype.replace()、String.prototype.search()、String.prototype.split()

        'a1a'.replace(/a/, 'b'); // 'b1a'
        'a1a'.replace(/a/g, 'b'); // 'b1b'
        ​
        'a1a'.search(/a/); // 0
        'a1a'.search(/a/g); // 0
        ​
        'a1a'.split(/a/); // ["", "1", ""]
        'a1a'.split(/a/g); // ["", "1", ""]
        
    1. 三个应用场景

      1. 正则与数值

        • 用正则处理数值

          数字判断

            /[0-9]+/
        

        []

        字符集,使用连字符 - 表示指定的字符范围,如果想要匹配连字符,需要挨着方括号放置,或进行转义

        0-9 表示匹配从 0 到 9 的数字字符,常用的还有 a-z 匹配小写字母,\u4e00-\u9fa5 匹配汉字等

        如果只是匹配数字,还可以使用字符集缩写 \d

        +

      限定符,匹配一个或多个

      缺点

      不是全字符匹配,存在误判,如 /[0-9]+/.test('a1') === true

           /^\d+$/
      
          **^**
      

      匹配字符串开始位置,当结合 m 修饰符时,匹配某一行开始位置

      $

      匹配字符串结束位置,当结合 m 修饰符时,匹配某一行结束位置

      缺点

      不能匹配带符号的数值,如 +1,-2

      不能匹配小数,如 3.14159

           /^[+-]?\d+(\.\d+)?$/
      

      ()

      不能匹配无整数部分的小数,如 .123

      捕获组会带来额外的开销

      圆括号内是一个子表达式,当圆括号不带任何修饰符时,表示同时创建一个捕获组

      ?

      这个正则的缺点

      ? 在正则中有多种含义,作为限定符时,表示匹配零到一个

      .

      可以匹配除换行符之外的任意字符,当结合 s 修饰符时,可以匹配包括换行符在内的任意字符

      当匹配小数点字符时需要转义

           /^[+-]?(?:\d*\.)?\d+$/
      

      (?:)

      不能匹配无小数部分的数值,如 2.

      不能匹配科学计数法,如 1e2、3e-1、-2.e+4

      创建一个非捕获组


      这个正则的缺点

      限定符,匹配零个或多个

      • 完整的数值正则写法

      完整的数值 token

      [https://drafts.csswg.org/css-syntax-3/#number-token-diagram(https://drafts.csswg.org/css-syntax-3/#number-token-diagram)

      [图片上传失败...(image-9e0708-1586495838965)]

      注意:这个 token 是 CSS 的 token,在 javascript 中,要多考虑一种情况

            +'2.'; // 2
            +'2.e1'; // 20
      
            **/^[+-]?(?:\d+\.?|\d\*\.\d+)(?: e[+-]?\d+)?$/i**
      

      |

      javascript 中,数值还可以如何表示?

      答案稍后给出

      用来创建分支,当位于圆括号内时,表示子表达式的分支条件,当位于圆括号外时,表示整个正则表达式的分支条件

      i 修饰符

      思考题:这个正则已经没有缺点了吗?

          表示匹配时忽略大小写,在这个例子中用于匹配科学计数法的 e,去掉 i 修饰符需要把 e 改为 [eE]
      
      • 用正则处理数值
            function execNumberList(str) {
             // ……
            }
            ​
            console.log(execNumberList('1.0px .2px -3px +4e1px')); // [1, 0.2, -3, 40]
            console.log(execNumberList('+1.0px -0.2px 3e-1px')); // [1, -0.2, 0.3]
            console.log(execNumberList('1px 0')); // [1, 0]
            console.log(execNumberList('-1e+1px')); // [-10]
      

      数值的解析

            const reg = /[+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?(?=px|\s|$)/gi;
            ​
            function execNumberList(str) {
             reg.lastIndex = 0;
             let exec = reg.exec(str);
             const result = [];
             while (exec) {
             result.push(parseFloat(exec[0]));
             exec = reg.exec(str);
             }
             return result;
            }
            ​
            console.log(execNumberList('1.0px .2px -3px +4e1px')); // [1, 0.2, -3, 40]
            console.log(execNumberList('+1.0px -0.2px 3e-1px')); // [1, -0.2, 0.3]
            console.log(execNumberList('1px 0')); // [1, 0]
            console.log(execNumberList('-1e+1px')); // [-10]
      

      数值的解析

      01

            const reg = /[+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?(?=px|\s|$)/gi;
      

      (?=expression)**

      正向肯定环视 / 顺序肯定环视 / 先行断言

      用于匹配符合条件的位置

      类似的语法还有:

      (?!*expression*) 正向否定环视 / 顺序否定环视 / 先行否定断言

      (?<=expression) 反向肯定环视 / 逆序肯定环视 / 后行断言,es2018 新增`

      (?<!*expression*) 反向否定环视 / 逆序否定环视 / 后行否定断言,es2018 新增

      const reg = /[+-]?(?:\d*\.)?\d+(?:e[+-]?\d+)?(?=px|\s|$)/gi;
      

      g

      • 按照 CSS 规范,只有数值为 0 才可以省略单位,这种情况没有必要靠正则来过滤

      • 这个例子中只验证了 px 单位,实际还存在 pt、em、vw 等单位,并且没有考虑百分比的情况

      • 实际工作中,要根据需求追加处理逻辑

      需要注意的点

      修饰符,表示全局匹配,用于取出目标字符串中所有符合条件的结果

      数值转货币格式

            function formatCurrency(str) {
             // ……
            }
            ​
            console.log(formatCurrency('1')); // 1
            console.log(formatCurrency('123')); // 123
            console.log(formatCurrency('12345678')); // 12,345,678
      
            const reg = /(\d)(?=(\d{3})+(,|$))/g;
            function formatCurrency(str) {
             return str.replace(reg, '$1,');
            }
            ​
            console.log(formatCurrency('1')); // 1
            console.log(formatCurrency('123')); // 123
            console.log(formatCurrency('12345678')); // 12,345,678</pre>
      
            <pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n524" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;">const reg = /(\d)(?=(?:\d{3})+(?:,|$))/g;
            function formatCurrency(str) {
             return str.replace(reg, '$1,');
            }
      

      {n}

      限定符,表示重复 n 次,n 必须是非负整数

      类似的语法还有:

      {n, m} 表示重复 n 到 m 次,n 和 m 都必须是非负整数,且 n <= m

      {n,} 表示重复 n 次以上

            const reg = /(\d)(?=(?:\d{3})+(?:,|$))/g;
            function formatCurrency(str) {
             return str.replace(reg, '$1,');
            }
      

      $n

      用于 replace 的字符串中,表示第 n 个捕获组,n 可以从 1 到 9

      $& 表示本次完整的匹配,所以这段代码还可以改写为:

            const reg = /\d(?=(?:\d{3})+(?:,|$))/g;
            function formatCurrency(str) {
             return str.replace(reg, '><,');
            }
      

      在 es2018 以上的环境,还可以使用反向环视

            const reg = /(?<=\d)(?=(?:\d{3})+(?:,|$))/g;
            function formatCurrency(str) {
             return str.replace(reg, ',');
            }
      

      其它注意事项

      环视中的圆括号也会生成捕获组,所以都要采用 (?:) 的非捕获组形式

      1. 正则与颜色

        16进制表示法

        color: #rrggbb;
        color: #rgb;
        color: #rrggbbaa;
        color: #rgba;
        const hex = '[0-9a-fA-F]';
        const reg = new RegExp(`^(?:#${hex}{6}|#${hex}{8}|#${hex}{3,4})<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n567" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;");
      

      对应的正则写法

      其它注意事项

      • 也可以使用 i 修饰符来匹配大小写,i 修饰符和 a-fA-F 要根据实际需求来做取舍

      rgb/rgba 表示法

        color: rgb(r, g, b);
        color: rgb(r%, g%, b%);
        color: rgba(r, g, b, a);
        color: rgba(r%, g%, b%, a);
        color: rgba(r, g, b, a%);
        color: rgba(r%, g%, b%, a%);
        const num = '[+-]?(?:\\d*\\.)?\\d+(?:e[+-]?\\d+)?';
        const comma = '\\s*,\\s*';
        const reg = new RegExp(`rgba?\\(\\s*${num}(%?)(?:${comma}${num}\\1){2}(?:${comma}${num}%?)?\\s*\\)`);
      

      对应的正则写法

        const num = '[+-]?(?:\\d*\\.)?\\d+(?:e[+-]?\\d+)?';
        const comma = '\\s*,\\s*';
        const reg = new RegExp(`rgba?\\(\\s*${num}(%?)(?:${comma}${num}\\1){2}(?:${comma}${num}%?)?\\s*\\)`);
      
       const num = '[+-]?(?:\\d*\\.)?\\d+(?:e[+-]?\\d+)?';
        const comma = '\\s*,\\s*';
        const reg = new RegExp(`rgba?\\(${num}(%?)(?:${comma}${num}\\1){2}(?:${comma}${num}%?)?\\)`);
      

      需要注意的点

      • 按照规范,rgb(r,g,b,a) 和 rgba(r,g,b) 也是合法的

      • r/g/b 的值应该是 0~255 的整数,但是溢出或小数并不会报错

      • 当捕获组内的内容是可选的时候,一定要把问号写在捕获组内 如果可选内容的圆括号不可省略,如(a|b|c)?,应该多嵌套一层:((?:a|b|c)?)

      \n

      反向引用,表示引用第 n 个捕获组

      由于 r/g/b 必须同时为数值或百分比,所以 %? 只需要捕获一次,用 \1 来引用

      \s

      字符集缩写,用于匹配空白

        /* hsl & hsla */
        color: hsl(h, s%, l%);
        color: hsla(h, s%, l%, a);
        color: hsla(h, s%, l%, a%);
        ​
        /* keywords */
        color: red;
        color: blue;
        /* …… */
      

      更多的颜色表示方法:

      https://www.w3.org/TR/css-color/

      16进制颜色的优化

        function shortenColor(str) {
         // ……
        }
        ​
        console.log(shortenColor('#336600')); // '#360'
        console.log(shortenColor('#19b955')); // '#19b955'
        console.log(shortenColor('#33660000')); // '#3600'
      
        const hex = '[0-9a-z]';
        const hexReg = new RegExp(`^#(?<r>${hex})\\k<r>(?<g>${hex})\\k<g>(?<b>${hex})\\k<b>(?<a>${hex}?)\\k<a><pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n626" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;", 'i');
        function shortenColor(str) {
         return str.replace(hexReg, '#$<r>$<g>$<b>$<a>');
        }
        ​
        console.log(shortenColor('#336600')); // '#360'
        console.log(shortenColor('#19b955')); // '#19b955'
        console.log(shortenColor('#33660000')); // '#3600'
      
        const hex = '[0-9a-z]';
        const hexReg = new RegExp(`^#(?<r>${hex})\\k<r>(?<g>${hex})\\k<g>(?<b>${hex})\\k<b>(?<a>${hex}?)\\k<a><pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n634" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;", 'i');
        function shortenColor(str) {
         return str.replace(hexReg, '#$<r>$<g>$<b>$<a>');
        }
      

      (?<key>)

      • es2018 新增,具名捕获组

      • 反向引用时的语法为 \k<key>

      • 在 replace 中,使用 $<key> 来访问具名捕获组

      • 当应用 exec 时,具名捕获组可以通过 execResult.groups[key] 访问

        const hex = '[0-9a-z]';
        const hexReg = new RegExp(`^#(?<r>${hex})\\k<r>(?<g>${hex})\\k<g>(?<b>${hex})\\k<b>(?<a>${hex}?)\\k<a><pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n645" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit;", 'i');
        ​
        hexReg.exec('#33660000');
        // ["#33660000", "3", "6", "0", "0", index: 0, input: "#33660000", groups: {r: "3", g: "6", b: "0", a: "0"}]
      
      1. 正则与URL

        解析url

        [图片上传失败...(image-4ada6a-1586494228370)]

        简单起见,scheme 我们只匹配 http 和 https ,忽略 userinfo 部分

        解析 URL

        function execURL(url) {
        // ……
        }
        ​
        console.log(execURL('https://www.360.cn'));
        {
        protocol: 'http:',
        host: 'www.360.cn',
        hostname: 'www.360.cn',
        port: '',
        pathname: '',
        search: '',
        hash: ''
        }
        console.log(execURL('http://localhost:8080/?#'));
        {
        protocol: 'http:',
        host: 'localhost:8080',
        hostname: 'localhost',
        port: '8080',
        pathname: '/',
        search: '?',
        hash: '#'
        }
        console.log(execURL('https://image.so.com/view?q=360&src=srp#id=9e17bd&sn=0'));
        {
        protocol: 'https:',
        host: 'image.so.com',
        hostname: 'image.so.com',
        port: '',
        pathname: '/view',
        search: '?q=360&src=srp',
        hash: '#id=9e17bd&sn=0'
        }
        console.log(execURL('this is not a url'));
        {
        protocol: '',
        host: '',
        hostname: '',
        port: '',
        pathname: '',
        search: '',
        hash: ''
        }
        
        const protocol = '(?<protocol>https?:)';
        const host = '(?<host>(?<hostname>[^/#?:]+)(?::(?<port>\\d+))?)';
        const path = '(?<pathname>(?:\\/[^/#?]+)*\\/?)';
        const search = '(?<search>(?:\\?[^#]*)?)';
        const hash = '(?<hash>(?:#.*)?)';
        const reg = new RegExp(`^${protocol}\/\/${host}${path}${search}${hash}
        
        
        function execURL(url) {
         const result = reg.exec(url);
         if (result) {
         result.groups.port = result.groups.port || '';
         return result.groups;
         }
         return {
         protocol: '', host: '', hostname: '', port: '',
         pathname: '', search: '', hash: '',
         };
        }
        ​
        console.log(execURL('https://www.360.cn'));
        console.log(execURL('http://localhost:8080/?#'));
        console.log(execURL('https://image.so.com/view?q=360&src=srp#id=9e17bd&sn=0'));
        console.log(execURL('this is not a url'));
        
        const host = '(?<host>(?<hostname>[^/#?:]+)(?::(?<port>\\d+))?)';
        ……
        function execURL(url) {
         const result = reg.exec(url);
         if (result) {
         result.groups.port = result.groups.port || '';
         return result.groups;
         }
         return {
         protocol: '', host: '', hostname: '', port: '',
         pathname: '', search: '', hash: '',
         };
        }
        

        注意事项

        • port 捕获组可能为 undefined

        • 要考虑解析失败的情形

        用正则解析 search 和 hash

        完整解析

      function execUrlParams(str) {
        // ……
       }
       ​
       console.log(execUrlParams('#')); // { }
       console.log(execUrlParams('##')); // { '#': '' }
       console.log(execUrlParams('?q=360&src=srp')); // { q: '360', src: 'srp' }
       console.log(execUrlParams('test=a=b=c&&==&a=')); // { test: 'a=b=c', '': '=', a: '' }
      
       function execUrlParams(str) {
        str = str.replace(/^[#?&]/, '');
        const result = {};
        if (!str) {
        return result;
        }
        const reg = /(?:^|&)([^&=]*)=?([^&]*?)(?=&|$)/y;
        let exec = reg.exec(str);
        while (exec) {
        result[exec[1]] = exec[2];
        exec = reg.exec(str);
        }
        return result;
       }
       ​
       console.log(execUrlParams('#')); // { }
       console.log(execUrlParams('##')); // { '#': '' }
       console.log(execUrlParams('?q=360&src=srp')); // { q: '360', src: 'srp' }
       console.log(execUrlParams('test=a=b=c&&==&a=')); // { test: 'a=b=c', '': '=', a: '' }
      
       const reg = /(?:^|&)([^&=]*)=?([^&]*?)(?=&|$)/y;
      

      *?

      ? 可以跟在任何限定符之后,表示非贪婪模式(注意:这个例子其实不太恰当,使用贪婪模式效果是一样的)
      const reg = /(?:^|&)([^&=]*)=?([^&]*?)(?=&|$)/y;
      y

      es6 新增,粘连修饰符,和 g 修饰符类似,也是全局匹配。区别在于:

      1. y 修饰符每次匹配的结果必须是连续的

      2. y 修饰符在 match 时只会返回第一个匹配结果

      其它注意事项

      正则表达式如果可能匹配到空字符串,极有可能造成死循环,所以这段代码很重要:

        if (!str) {
        return result;
        }
      

      解析指定 key

      问题

       function getUrlParam(str, key) {
        // ……
       }
       ​
       console.log(getUrlParam('?nothing', 'test')); // ''
       console.log(getUrlParam('#a=1&aa=2&aaa=3', 'a')); // '1'
       console.log(getUrlParam('&b=1&a=1&b=2', 'b')); // '2'
       console.log(getUrlParam('a=1&b=2&c=&d', 'c')); // ''
       console.log(getUrlParam('&d==', 'd')); // '='
       ```
       **注意事项**
      
       *   存在多个重复的 key 时,要求只返回最后一条匹配的结果
      
       *   挑战1:解法不止一种,你可以写出尽可能多的解法吗?
      
       *   挑战2:可以写出尽可能短的正则表达式吗?
      
      
    2. 总结

      怎样用好正则表达式?

      • 明确需求

      • 考虑全面

      • 反复测试

    相关文章

      网友评论

          本文标题:第六届360前端星计划_正则的三个应用场景

          本文链接:https://www.haomeiwen.com/subject/raxrmhtx.html