JavaScript正则表达式迷你书读书笔记

34次阅读

共计 17905 个字符,预计需要花费 45 分钟才能阅读完成。

正则基础

常见简写形式

字符组 具体含义 记忆方式
d 表示 [0-9]。表示是一位数字。 其英文是 digit(数字)
D 表示 1。表示除数字外的任意字符。
w 表示 [0-9a-zA-Z_]。表示数字、大小写字母和下划线。 w 是 word 的简写,也称单词字符。
W 表示 2。非单词字符。
s 表示 [tvnrf]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页 符。 s 是 space 的首字母,空白符的单词是 white space。
S 表示 3。非空白符。
. 表示 4。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符 除外。 想想省略号 … 中的每个点,都可以理解成占位符,表示任何类似的东西。
[dD]/[wW]/[sS]/[^] 任意字符

量词

字符组 具体含义 记忆方式
{m,} 至少出现 m 次
{m} 等价于{m, m}, 出现 m 次
? 等价于{0, 1}, 出现或者不出现 问号的意思: 有么?
+ 等价于{1,}, 表示至少出现一次 加号是追加的意思,表示必须现有一个,然后才考虑追加
* 等价于{0,}, 出现任意次, 有可能不出现 看看天上的星星,可能一颗没有,可能零散有几颗,可能数也数不过来

修饰符

字符组 具体含义 记忆方式
g 匹配全局 单词是 global
i 忽略字母大小写 单词是 ignoreCase
m 多行匹配, 只影响 ^ 和 $,二者变成行的概念,即行开头和行结尾 单词是 multiline

匹配位置

字符组 具体含义 记忆方式
^ (脱字符)匹配开头
$ (美元符号)匹配结尾
b 单词边界 w 与 W 之间的位置,也包括 w 与 ^ 之间的位置,和 w 与 $ 之间的位置
B 非单词边界
(?=p) “p” 的子模式, “p” 前面的位置 positive lookahead(正向先行断言)
(?!p) 非 ”p” 前的位置 negative lookahead(负向先行断言)

匹配开头与结尾

  • /^|$/g: 匹配列
  • /^|$/gm: 匹配行, m是既有修饰符

ES5 之后版本支持的位置判断

  • (?<=p): positive lookbehind(正向后行断言)
  • (?<!p): negative lookbehind(负向后行断言)

举例

  • 千分位

    • 千分位示例1234567890

      • 三位数字的前面: /(?=\d{3}$)/g

        • (?=p): 正向先行断言
      • 多个三位数字: /(?=(\d{3})+$)/g

        • +: 量词, 多个
      • 最前面不匹配: /(?!^)(?=(\d{3})+$)/g

        • (?!^): 负向先行断言
      • 非捕获:

        • 正则: /\B(?=(?:\d{3})+(?!\d))/g

          • 千位分隔符的理解
    • 带空格的千分位123456789 123456789

      • /(?!\b)(?=(\d{3})+\b)/g,即/(\B)(?=(\d{3})+\b)/g
      • ^$ 要替换成\b
    • 格式化千分位

      function format (num) {return num.toFixed(2).replace(/\B(?=(\d{3})+\b)/g, ",").replace(/^/, "$$");
      };
      console.log(format(1888) );
      // => "$ 1,888.00"
  • 验证密码: 长度为 6~12 位, 由数字、大写字母和小写字母,必须至少包含两种字符

    • 长度为 6~12 位的数字、大小写字母: /^[0-9a-zA-Z]{6, 12}$/
    • 至少包含两种

      • 解题核心

        • 至少包含一位数字字符: /(?=.*[0-9])/
      • 划分情况

        • 包含数字和大写字母
        • 包含数字和小写字母
        • 包含大写字母和小写字母
        • 包含数字、大写字母和小写字母
      • 正则: /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[0-9a-zA-Z]{6,12}$/

        • 注意: {6,12}中间不能有空格
    • 不能同时全部为一种(反向思路)

      • 解题核心

        • 不能全部为一种字符(数字): /(?!^[0-9]{6,12}$)(^[0-9a-zA-Z]{6,12})/
      • 划分情况

        • 不能全为数字
        • 不能全为大写字母
        • 不能全为小写字母
      • 正则: /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)(^[0-9a-zA-Z]{6,12}$)/

常用正则

词法 含义 记忆方式
(?=.[A-Z]) / (?=.?[A-Z]) / (.*[A-Z]) 至少包含一个大写字母

括号的作用

分组和分支

  • 分组: 'ababa abbb ababab'.match(/(ab)+/g)
  • 分支: (p1|p2)

    • 示例

      var regex = /^I love (JavaScript|Regular Expression)$/
      console.log(regex.test("I love JavaScript")) // true
      console.log(regex.test("I love Regular Expression")) // true

分组引用: 正则引擎给分组开辟了一个空间, 用来存储每个分组匹配到的数据

  • 分组方法

    • 之前: /^d{4}-d{2}-d{2}$/
    • 之后: /^(d{4})-(d{2})-(d{2})$/
  • 提取数据:

    • match 方法

      • 使用: '2017-06-12'.match(/(\d{4})-(\d{2})-(\d{2})/)
      • match 返回数组: ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12", groups: undefined]

        • 第一个是整体匹配结果
        • 然后是各个分组匹配到的结果
        • 匹配下标
        • 输入的文本
        • 一个捕获组数组 或 undefined(如果没有定义命名捕获组)
      • 注意:

        • 使用 g 之后, 返回的是一个数组

          • 举例: '1s1'.match(/\d/g) => ["1", "1"]
        • 匹配不到, 则返回null
    • exec 方法

      • 使用: /(\d{4})-(\d{2})-(\d{2})/.exec('2017-06-12')
    • 正常操作 (test/match/exec) 之后, 可以通过 RegExp$0~$9获取
  • 替换: 转换 yyyy-mm-dddd/mm/yyyy

    • $方法

      '2019-09-23'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1')
    • RegExp.$+function方法

      '2019-09-23'.replace(/(\d{4})-(\d{2})-(\d{2})/, function() {return `${RegExp.$2}/${RegExp.$3}/${RegExp.$1}`
      })
    • function方法

      '2019-09-23'.replace(/(\d{4})-(\d{2})-(\d{2})/, function(match, year, month, day) {return `${month}/${day}/${year}`
      })

反向引用: \1

  • 同时匹配日期格式: 2019-09-24, 2019/09/24, 2019.09.24

    • 正则: /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/
    • 问题: 格式不统一 , 2019/09-24 同样能匹配
    • 解决方法: 使用反向引用

      • 正则: /\d{4}(-|\/|\.)\d{2}(\1)\d{2}/
    • 同理: \2是第二个分组引用, 依次类推
  • 括号嵌套

    • 正则

      // ["1231231233", "123", "1", "23", "3", index: 0, input: "1231231233", groups: undefined]
      1231231233'.match(/^((\d)(\d(\d)))\1\2\3\4$/) 
    • 拆分

      • /^((\d)(\d(\d)))$/.test('123') // true
      • /^(\d)$/.test('1') // true
      • /^(\d(\d))$/.test('23') // true
      • /^(\d)$/.test('3') // true, 同第二个一样
  • 引用不存在的分组

    • 会匹配反向引用本身

      • 正则: /\1/.test('\1') // true
  • 分组后由量词

    • 会匹配到最后一次匹配

      • 正则: '12345'.match(/(\d)+/) // ["12345", "5", index: 0, input: "12345", groups: undefined]
      • 对于反向引用, 同理: /(\d)+ \1/.test('12345 5') // true

非捕获括号: (?:p)

非捕获匹配到的值不会保存起来

  • 与之相反的是

    • 捕获型分组
    • 捕获型分支
  • 举例

    'ababa abbb ababab'.match(/(?:ab)+/g) // ["abab", "ab", "ababab"]
    var regex = /^I love (?:JavaScript|Regular Expression)$/
    regex.test("I love JavaScript") // true
    regex.test("I love Regular Expression") // true

实战应用

trim: 去掉头部和尾部的空字符串

  • js: 'hello'.trim() // 'hello'
  • 正则:

    • 匹配到开头和结尾的空字符串, 替换掉 ( 效率高)

      'hello'.replace(/^\s+|\s+$/g, '')
    • 惰性匹配*?, 匹配所有字符串, 提取相应数据

      'hello'.replace(/^\s*(.*?)\s*$/, '$1') // 'hello'

将每个单词的首字母转换为大写

  • 方法

    function titleize (str) {return str.toLowerCase().replace(/(?:^|\s)\w/g, function (c) {return c.toUpperCase();
        });
    }
    // My Name Is Epeli
    console.log(titleize('my name is epeli') )
  • 注意

    • \s: 制表符空格等, 用来匹配 name 这样的数据

驼峰

  • 方法

    var camelize = function(str) {return str.replace(/[-_\s]+(.)?/g, function(match, c){return c ? c.toUpperCase() : ''
        })
    }
    console.log(camelize('-moz-transform'))
  • 注意

    • replace: 关于 replace 参数

      • match: 匹配到的字符串
      • p1,p2,...: 代表第 n 个括号匹配到的字符串
      • offset: 匹配到字符串在原字符串的偏移量
      • string: 被匹配的原字符串
      • NamedCaptureGroup: 命名捕获组匹配的对象
    • [-_\s]: 连字符、下划线和空白符
    • ?: 应对单词结尾不是单词字符, 例如'-moz-transform'

中划线化

  • 方法

    var dasherize = function(str) {return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase()}
    console.log(dasherize('MozTTransform'))
  • 或者使用function

    var sperateLine = function(str) {return str.replace(/[A-Z]{1}/g, function(match) {return match ? `-${match.toLowerCase()}` : ''
        })
    }
    console.log(sperateLine('MozTTransform'))

HTML 转义和反转义

  • 转义

    • 方法

      var escapeHTML = function(str) {
          var escapeChars = {
              '<': 'lt',
              '>': 'gt',
              '"':'quot','&':'amp','\'':'#39'
          }
          return str.replace(new RegExp(`[${Object.keys(escapeChars).join('')}]`,'g'), function(match) {return `&${escapeChars[match]};`
          })
      }
      console.log(escapeHTML('<div>hello, \'world\'</div>'))
  • 反转义

    • 方法

      var unescapeHTML = function(str) {
          var htmlEntities = {
              'lt': '<',
              'gt': '>',
              'quot': '"','amp':'&','#39':'\''
          }
          return str.replace(new RegExp(`\&([^;]+);`, 'g'), function(match, key) {console.log(match, key)
              return (key in htmlEntities) ? htmlEntities[key] : match
          })
      }
      console.log(unescapeHTML('&lt;div&gt;hello, &#39;world&#39;&lt;/div&gt;'))
    • 注意

      • 关于 /\&([^;]+);/g^

        • 匹配输入字符串开头位置
        • 方括号中使用, 表示不接受该字符集合
        • 匹配 ^ 本身,使用\^
        • 图示:
  • 成对标签

    • 简单匹配正则: /<([^>]+)>[\d\D]*<\/\1>/
    • 验证: /<([^>]+)>[\d\D]*<\/\1>/.test('<title>wrong!</p>') // false

回溯法

  • 没有回溯的匹配

    • 举例: /ab{1,3}c/.test('abbbc')
  • 有回溯的匹配

    • 举例: /ab{1,3}c/.test('abbc')
  • 回溯产生的原因: 一次没有恰好匹配, 需要多次匹配
  • 建议: 应该避免

    • 举例

      // .* 任意字符出现任意次, 会匹配到 abc"de, 匹配完之后发现还有" 正则, 然后会回溯,只匹配到 abc
      /".*"/.test('"abc"de')
      // 更改建议, 匹配非 "的字符任意次, 碰到" 就终止匹配, 减少回溯, 提高效率
      /"[^"]*"/.test('"abc"de')
  • 释义

    • 百度百科

      回溯法也称试探法,它的基本思想是: 从问题的某一种状态 (初始状态) 出发,搜索从这种状态出发 所能达到的所有“状态”,当一条路走到“尽头”的时候 (不能再前进),再后退一步或若干步,从 另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态) 都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”。

    • 本质: 深度优先搜索算法
    • 回溯: 退到之前的某一步的过程
  • 常见的回溯形式

    • 贪婪量词

      • 尝试顺序从多往少的方向去尝试
      • 举例

        // ["12345", "123", "45", index: 0, input: "12345", groups: undefined]
        '12345'.match(/(\d{1,3})(\d{1,3})/)
    • 惰性量词

      • 贪婪量词后加问号尽可能少的匹配
      • 举例

        // ["1234", "1", "234", index: 0, input: "12345", groups: undefined]
        '12345'.match(/(\d{1,3}?)(\d{1,3})/)
      • 会回溯的惰性

        // ["12345", index: 0, input: "12345", groups: undefined]
        '12345'.match(/^\d{1,3}?\d{1,3}$/)
    • 分支结构

      • 分支也是惰性匹配

        // ["can", index: 0, input: "candy", groups: undefined]
        'candy'.match(/can|candy/)
      • 整体匹配的话,也会回溯

        // ["candy", index: 0, input: "candy", groups: undefined]
        'candy'.match(/^(?:can|candy)$/)
    • 总结

      • 回溯相对 DFA 引擎, 匹配效率低一些
      • 概念

        • DFA: 确定型有限自动机
        • NFA: 非确定型有限自动机

          • JavaScript 就是, 而且流行, 匹配慢(相对 DFA)
          • 流行原因: 编译快, 有趣?

拆分

  • 结构的具体含义

    结构 说明 举例
    字面量 匹配一个具体字符,包含需要转义和不需要转义的 不转义: /a/匹配字符 'a'; 转义: /\n/ 匹配换行符, \.匹配小数点
    字符组 匹配一个字符,可以是多种可能之一;反义字符组,表示除特定字符的其他字符 /[0-9]/表示匹配一个数字, 简写形式 /\d/; [^0-9] 表示匹配一个非数字, 简写形式/\D/
    量词 表示一个字符连续出现; 或常见的简写形式 /a{1,3}/表示 a 字符连续出现 1~3 次; /a+/表示 a 出现至少一次
    匹配一个位置, 而不是字符 ^表示匹配位置的开头, \b表示匹配单词边界, (?=\d)表示数字的前面
    分组 用括号表示一个整体 (ab)+表示 ab 两个字符连续出现多次, 也可以使用非捕获组(?:ab)+
    分支 多个子表达式多选一, 反向引用 `abc bcd表示匹配 ‘abc’ 或者 ‘bcd’ 字符子串; 2` 表示引用第 2 个分组
  • 操作符(从上至下)

    操作符描述 操作符 优先级
    转义符 \ 1
    括号和方括号 (...)(?:...)(?=...)(?!...)[...] 2
    量词限定符 {m}{m,n}{m,}?*+ 3
    位置和序列 ^$\ 元字符 一般字符 4
    管道符(竖杠) ` ` 5
    • 举例: /ab?(c|de*)+|fg/
  • 注意

    • 匹配字符串整体问题

      • 错误示例: /^abc|bcd$/
    ![QQ20191010-145021.png](https://image-static.segmentfault.com/216/764/2167645415-5da548b931259_articlex)
    - 正确示例: `/^(abc|bcd)$/`
    ![QQ20191010-145048.png](https://image-static.segmentfault.com/757/272/757272158-5da548b9cd455_articlex)
    - 原因: 位置操作符优先级高于管道操作符
- 量词连缀问题
    - 每个字符为 `'a'`,`'b'`,`'c'` 任选其一, 字符串长度是 3 的倍数
        - 错误示例: `/^[abc]{3}+$/`
            > 会报错, 说 `+` 前没有什么可重复的
            
        - 正确示例: `/([abc]{3})+/`
        ![QQ20191010-150526.png](https://image-static.segmentfault.com/207/251/2072516231-5da548ba4ba73_articlex)
- 元字符转义问题
    - 元字符
        ```
        /\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,/.test('^$.*+?|\\/[]{}=!:-,')
        ```
    - 字符串中, 每个字符转义之后还是本身
        ```
        // true
        '^$.*+?|\\/[]{}=!:-,' === '\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,'
        ```
        - 不是每个字符都需要转义
            - 跟字符组有关的 `[`、`]`、`^`、`-`。因此在会引起歧义的地方进行转义, 例如 `^`, 否则会把整个字符组看成反义字符组
                ```
                '^$.*+?|\\/[]{}=!:-,'.match(/[\^$.*+?|\\/\[\]{}=!:\-,]/g)
                ```
            - 匹配 `'[abc]'` 和 `{3,5}`
                ```
                /\[abc]/.test('[abc]')
                /\{3,5}/.test('{3,5}')
                /{,3}/.test('{,3}')
                ```
                - 原因: * 只需要在第一个方括号转义即可,因为后面的方括号构不成字符组,正则不会引发歧义,自然不需要转义 *
            - 其他
                - `=`, `!`, `:`, `-`, `,` 不在特殊结构, 不需要转义
                - 括号前后需要转义, `/\(123\)/`
                - `^`, `$`, `.`, `*`, `+`, `?`, `|`, `\`, `/` 等字符, 只要不在字符组内, 都需要转义
- 案例分析
    - 正则: `/^(\d{15}|\d{17}[\dxX])$/`
    - IPV4 正则: `/^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/`
        - 结构: `((...)\.){3}(...)`
        - 结构匹配: `3 位数.3 位数.3 位数.3 位数 `
        - 拆分
            - `0{0,2}\d`: 匹配
                ```
                0~9
                // 或
                00~09
                // 或
                000~009
                ```
            - `0?\d{2}`: 匹配
                ```
                10~99
                // 或
                010~099
                ```
            - `1\d{2}`: 匹配 `100`~`199`
            - `2[0-4]\d`: 匹配 `200`~`249`
            - `25[0-5]`: 匹配 `250`~`255`

正则表达式构建

  • 平衡法则

    • 匹配预期的字符串
    • 不匹配非预期的字符串
    • 可读性和可维护性
    • 效率
  • 构建正则前提

    • 是否能使用正则

      • 举例: '1010010001...'
    • 是否有必要使用 (复杂) 正则

      • 字符串分隔举例

        var string = '2017-07-01'
        // 正则
        var reg = /^(\d{4})-(\d{2})-(\d{2})/
        console.log(string.match(reg))
        // js api
        var stringArray = string.split('-')
        console.log(stringArray)
      • 判断是否有问号

        var string = '?id=xx&act=search'
        // 正则
        console.log(string.search(/\?/))
        // js api
        console.log(string.indexOf('?'))
      • 获取子串

        var string = 'JavaScript'
        // 正则
        var reg = /.{4}(.+)/
        console.log(string.match(reg)[1])
        // js api
        console.log(string.substring(4))
      • 是否有必要构建一个复杂的正则

        • 密码问题: 长度 6~12 位, 由数字、小写和大写字符组成, 必须包含两种字符
        • 复杂正则: /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/
        • 分拆简单正则:

          var regex1 = /^[0-9a-zA-Z]{6,12}$/
          var regex2 = /^[0-9]$/
          var regex3 = /^[a-z]$/
          var regex4 = /^[A-Z]$/
          function checkPassword = function(string) {if (!regex1.test(string) return false
              if (regex2.test(string)) return false
              if (regex3.test(string)) return false
              if (regex4.test(string)) return false
              return true
          }
  • 准确性

    • 问题 1:

      • 描述:匹配如下格式固话0551888888880551-88888888(0551)88888888
      • 分析(不考虑分机号和+86)

        • 区号, 以 0 开头的 3~4 位数字, 正则: /0\d{2,3}/
        • 号码, 非 0 开头的 6~7 位数字, 正则: /[1-9]\d{6,7}/
        • 匹配055188888888, 正则: /^0\d{2,3}[1-9]\d{6,7}$/
        • 匹配0551-88888888, 正则: /^0\d{2,3}-[1-9]\d{6,7}$/
        • 匹配(0551)88888888, 正则: /^\(0\d{2,3}\)[1-9]\d{6,7}$/
        • 合并正则: ^/0\d{2,3}[1-9]\d{6,7}|0\d{2,3}-[1-9]\d{6,7}|\(0\d{2,3}\)[1-9]\d{6,7}$/
        • 提取公共正则: /^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/
        • 进一步简写: /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/
        ![QQ20191011-121606.png](https://image-static.segmentfault.com/365/885/3658857746-5da548bae93e5_articlex)
    - 测试
        ```
        /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/.test('055188888888')
        /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/.test('0551-88888888')
        /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/.test('(0551)88888888')
        /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/.test('051-8888888')
        ```
    - 问题
        - 3 位和 4 位数字不一定是真实区号
- 问题 2:
    - 描述: 匹配如下格式浮点数 `1.23`、`+1.23`、`-1.23`、`10`、`+10`、`-10`、`.2`、`+.2`、`-.2`
    - 分析
        - 符号部分: `[+-]`
        - 整数部分: `\d+`
        - 小数部分: `\.\d+`
        - 匹配 `1.23`、`+1.23`、`-1.23` 正则: `/^[+-]?\d+\.\d+$/`
        - 匹配 `10`、`+10`、`-10` 正则: `/^[+-]?\d+$/`
        - 匹配 `.2`、`+.2`、`-.2` 正则: `/^[+-]?\.\d+$/`
        - 合并正则: `/^[+-]?(\d+\.\d+|\d+|\.\d+)$/`
        ![float_1.png](https://image-static.segmentfault.com/386/026/3860260627-5da548bb9d7f5_articlex)
        - 另外一种写法: `/^[+-]?(\d+)?(\.)?\d+$/`
            > 涉及到可维护性和可读性
            
  • 效率

    • 正则运行阶段

      • 编译

        引擎报错与否在这个阶段

      • 设定起始位置
      • 尝试匹配

        可以优化的阶段

      • 匹配失败,从下一位开始继续第 3 步

        可以优化的阶段

      • 最终结果: 匹配成功或失败
    • 运行代码示例

      var regex = /\d+/g;
      // 0 ["123", index: 0, input: "123abc34def", groups: undefined]
      console.log(regex.lastIndex, regex.exec("123abc34def") );
      // 3 ["34", index: 6, input: "123abc34def", groups: undefined]
      console.log(regex.lastIndex, regex.exec("123abc34def") );
      // 8 null
      console.log(regex.lastIndex, regex.exec("123abc34def") ); 
      // 0 ["123", index: 0, input: "123abc34def", groups: undefined]
      console.log(regex.lastIndex, regex.exec("123abc34def") );

      当使用 testexec时, 正则有 g 时, 起始位置是从 lastIndex 开始的

    • 优化方法

      • 使用具体型字符组代替通配符, 来消除回溯

        • /".*"/.test('123"abc"456'): 回溯有 4 次
        • /".*?"/.test('123"abc"456'): 回溯有 2 次

          • 惰性匹配*?
        • /"[^"]*"/.test('123"abc"456'): 无回溯
      • 使用非捕获分组

        不需要使用分组引用和反向引用时

        • /^[+-]?(\d+\.\d+|\d+|\.\d+)$/=>/^[+-]?(?:\d+\.\d+|\d+|\.\d+)$/
      • 独立出确定字符

        • /a+/ => /aa*/
      • 提取公共分支部分

        可减少匹配过程中的重复

        • /^abc|^bcd/ => /^(abc|bcd)/
        • /this|that/ => /th(?:is|at)/
      • 减少分支的数量, 缩小它们范围

        • /red|read/ => /rea?d/: 可读性降低
    • 总结

      • 关于准确性: 满足需求即可
      • 关于效率: 看个人要求
      • 准确性思路

        • 针对每种情形, 分别写出正则
        • 再用分支进行合并
        • 提取公共部分

正则表达式编程

  • 匹配: 目标字符串中是否有匹配的子串
  • 正则表达式的四种操作

    • 验证: 判断是否的操作

      • search

        // true
        !!~'abc123'.search(/\d/)
      • match

        // true
        !!'abc123'.match(/\d/)
      • test

        // true
        /\d/.test('abc123')
      • exec

        // true
        !!/\d/.exec('abc123')
    • 切分:

      • 切分 , 分隔的字符串

        // ["html", "js", "css"]
        'html,js,css'.split(/,/)
      • 日期切割

        // ["2019", "10", "11"]
        '2019/10/11'.split(/\D/)
        '2019.10.11'.split(/\D/)
        '2019-10-11'.split(/\D/)
    • 提取:

      • search

        '2019-10-11'.search(/^(\d{4})\D(\d{2})\D(\d{2})$/)
        // 2019 10 11
        console.log(RegExp.$1, RegExp.$2, RegExp.$3)
      • match

        // ["2019-10-11", "2019", "10", "11", index: 0, input: "2019-10-11", groups: undefined]
        '2019-10-11'.match(/^(\d{4})\D(\d{2})\D(\d{2})$/)
      • replace

        var date = []
        '2019-10-11'.replace(/^(\d{4})\D(\d{2})\D(\d{2})$/, function(year, month, day) {date.push(year, month, day)
        })
        // ["2019-10-11", "2019", "10"]
        console.log(date)
      • test

        /^(\d{4})\D(\d{2})\D(\d{2})$/.test('2019-10-11')
        // 2019 10 11
        console.log(RegExp.$1, RegExp.$2, RegExp.$3)
      • exec

        /^(\d{4})\D(\d{2})\D(\d{2})$/.exec('2019-10-11')
        // 2019 10 11
        console.log(RegExp.$1, RegExp.$2, RegExp.$3)
    • 替换: 需要重点掌握

      var tody = new Date('2019-10-11'.replace(/-/g, '/'))
      // Fri Oct 11 2019 00:00:00 GMT+0800 (China Standard Time)
      console.log(tody)
  • 相关 API 注意要点

    • searchmatch 会把字符串转成正则

      // 0
      '2019.10.11'.search('.')
      // ["2", index: 0, input: "2019.10.11", groups: undefined]
      '2019.10.11'.match('.')
      // 4
      '2019.10.11'.search('\\.')
      // [".", index: 4, input: "2019.10.11", groups: undefined]
      '2019.10.11'.match('\\.')
      // 4
      '2019.10.11'.search(/\./)
      // [".", index: 4, input: "2019.10.11", groups: undefined]
      '2019.10.11'.match(/\./)
      // "2019/10.11"
      '2019.10.11'.replace('.', '/')
    • match返回的格式问题: 与是否有修饰符 g 有关

      // ["2019", "2019", index: 0, input: "2019.10.11", groups: undefined]
      console.log('2019.10.11'.match(/\b(\d+)\b/))
      // ["2019", "10", "11"]
      console.log('2019.10.11'.match(/\b(\d+)\b/g))
    • execmatch 更强大: match使用 g 之后, 没有关键信息 index, exec 可以解决这个问题, 并且接着上一次继续匹配

      var string = "2019.10.11";
      var regex2 = /\b(\d+)\b/g;
      // ["2019", "2019", index: 0, input: "2019.10.11", groups: undefined]
      console.log(regex2.exec(string) );
      // 4
      console.log(regex2.lastIndex);
      // ["10", "10", index: 5, input: "2019.10.11", groups: undefined]
      console.log(regex2.exec(string) );
      // 7
      console.log(regex2.lastIndex);
      // ["11", "11", index: 8, input: "2019.10.11", groups: undefined]
      console.log(regex2.exec(string) );
      // 10
      console.log(regex2.lastIndex);
      // null
      console.log(regex2.exec(string) );
      // 0
      console.log(regex2.lastIndex);
      • 对使用 exec 用法优化

        示例中 lastIndex 表示下次匹配的开始位置

        var string = '2019.10.11'
        var regex = /\b(\d+)\b/g
        var result
        while (result = regex.exec(string)) {console.log(result, regex.lastIndex)
        }
    • 修饰符 gexectest 的影响

      • 字符串的四个方法, 每次匹配都是从 0 开始, 即 lastIndex 属性始终不变

        var regex = /a/g
        'a'.search(regex)
        // 0
        console.log(regex.lastIndex)
        'ab'.search(regex)
        // 0
        console.log(regex.lastIndex)
      • 正则的 exectest方法, 当正则含有g, 每次匹配都会更改lastIndex; 不含g, 则不会改变lastIndex

        var regex = /a/g
        // true 1
        console.log(regex.test('a'), regex.lastIndex )
        // true 3
        console.log(regex.test('abac'), regex.lastIndex )
        // false 0
        console.log(regex.test('abacd'), regex.lastIndex )
    • test整体匹配时需要 ^$

      test是看目标字符串中是否有子串符合条件

      // true
      /123/.test('a123b')
      // false
      /^123$/.test('a123b')
      // true
      /^123$/.test('123')
  • split相关事项

    • 有 2 个参数, 第 2 个表示数组最大长度

      // ["js", "css"]
      'js,css,html'.split(/,/, 2)
    • 使用分组, 则结果包含分隔符本身

      // ["js", ",", "css", ",", "html"]
      'js,css,html'.split(/(,)/)
  • replace很强大

    • 第二个参数是字符串时, 有如下含义

      属性 描述
      $1,$2,…,$99 匹配第 1~99 个分组里捕获到的文本
      $& 匹配到的子串文本
      $` 匹配到的子串左边的文本
      $’ 匹配到的子串右边的文本
      $$ 美元符号
      • 2,3,5 变成5=2+3

        // "5=2+3"
        '2,3,5'.replace(/(\d+),(\d+),(\d+)/, '$3=$1+$2')
      • 2,3,5 变成222,333,555

        '2,3,5'.replace(/(\d+)/g, '$&$&$&')
      • 2+3=5 变成2+3=2+3=5=5

        // "2+3=2+3=5=5"
        '2+3=5'.replace(/(=)/, "$&$`$&$'$&")
    • 第二个参数是函数时

      replace此时拿到的信息比 exec

      // ["1234", "1", "4", 0, "1234 2345 3456"]
      // ["2345", "2", "5", 5, "1234 2345 3456"]
      // ["3456", "3", "6", 10, "1234 2345 3456"]
      "1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (match, $1, $2, index, input) { 
          // $1 是每组数字的开始
          // $2 是每组数字的结束
          console.log([match, $1, $2, index, input]);
      })
  • 使用构造函数需要注意的问题

    不推荐使用, 会写很多的\

    // ["2017-06-27", "2017.06.27", "2017/06/27"]
    '2017-06-27 2017.06.27 2017/06/27'.match(/\d{4}(-|\.|\/)\d{2}\1\d{2}/g)
    // ["2017-06-27", "2017.06.27", "2017/06/27"]
    '2017-06-27 2017.06.27 2017/06/27'.match(new RegExp('\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}', 'g'))
  • 修饰符

    修饰符 描述 单词
    /g/ 全局匹配(找到所有的) global
    /i/ 忽略字母大小写 ignoreCase
    /m/ 多行匹配, 只影响 ^$, 二者变成行概念(行开头、行结尾) multiline
    • 只读属性

      var regex = /\w/img
      // true
      console.log(regex.global)
      // true
      console.log(regex.ignoreCase)
      // true
      console.log(regex.multiline)
  • source属性

    对象属性, 除了 global, ignoreCase, multiline, lastIndex 还有 source 属性; 用来构建动态正则, 或确认真正的正则

    var className = "high";
    // => (^|\s)high(\s|$) 
    // => 即字符串 "(^|\\s)high(\\s|$)"
    var regex = new RegExp("(^|\\s)" + className + "(\\s|$)"); console.log(regex.source)
    console.log(regex.test('high'), regex.test('high'))
  • 构造函数属性

    静态属性随着最后一次正则操作而变化, 除了$1,...$9, 还有几个不太常用的(有兼容问题)

    静态属性 描述 简写形式
    RegExp.input 最近一次目标字符串 RegExp[‘$_’]
    RegExp.lastMatch 最近一次匹配的文本 RegExp[‘$&’]
    RegExp.lastParen 最近一次捕获的文本 RegExp[‘$+’]
    RegExp.leftContext 目标匹配 lastMatch 之前的文本 RegExp[‘$`’]
    RegExp.rightContext 目标匹配 lastMatch 之后的文本 RegExp[“$'”]
    var regex = /([abc])(\d)/g
    var string = 'a1b2c3d4e5'
    string.match(regex)
    
    // a1b2c3d4e5 a1b2c3d4e5
    console.log(RegExp.input, RegExp['$_'])
    // c3 c3
    console.log(RegExp.lastMatch, RegExp['$&'])
    // 3 3
    console.log(RegExp.lastParen, RegExp['$+'])
    // a1b2 c3
    console.log(RegExp.leftContext, RegExp['$&'])
    // d4e5 d4e5
    console.log(RegExp.rightContext, RegExp["$'"])

真实案例

  • 构造函数生成正则: 通过 class 获取 dom

    function getClassByName(className) {var elements = document.getElementByTagName('*')
        var regex = new RegExp('(^|\\s)' + className + '($|\\s)')
        var result = []
        var elementsLength = elements.length
        for (var i = 0; i < elementsLength; i++) {var element = elements[i]
            if (regex.test(element.className)) {result.push(element)
            }
        }
        return result
    }
  • 字符串保存数据: 判断数据类型

    var utils = {}
    'Boolean|Number|String|Function|Array|Date|RegExp|Object|Error'.split('|').forEach(function(item) {utils['is' + item] = function(obj) {return {}.toString.call(obj) === '[object'+ item +']'
        }
    })
    console.log(utils.isArray([1, 2, 3]))
  • 正则替代 &&(不兼容 IE)

    var readyRegex = /complete|loaded|interactive/
    function ready(callback) {if (readyRegex.test(document.readyState) && document.body) {callback()
        }
        else {document.addEventListener('DomContentLoaded', function() {callback()
            }, false)
        }
    }
  • 强大的 replace: 参数查询压缩

    function compress(source) {var keys = {}
        // [^=&]中的 & 不能去掉, 否则第二次会匹配成 &b=2
        // key 不能为空, value 有可能为空, 所以第一次是 +, 第二次是 *
        source.replace(/([^=&]+)=([^&])*/g, function(full, key, value) {keys[key] = (keys[key] ? keys[key] + ',' : '') + value
        })
        var result = []
        for (var key in keys) {result.push(key + '=' + keys[key])
        }
        return result.join('&')
    }
    // a=1,3&b=2,4
    console.log(compress('a=1&b=2&a=3&b=4'))
  • 根据字符串生成正则

    function createRegex(regex) {
        try {if (regex[0] === '/') {regex = regex.split('/')
                regex.shift()
                var flags = regex.pop()
                regex = regex.join('/')
                regex = new RegExp(regex, flags)
            }
            else {regex = new RegExp(regex, 'g')
            }
            return {
                success: true,
                result: regex
            }
        }
        catch (e) {
            return {
                success: false,
                message: '无效的正则表达式'
            }
        }
    }
    // {success: true, result: /\d/gm}
    console.log(createRegex('/\\d/gm'))
    // {success: true, result: /\d/g}
    console.log(createRegex('/\\d/g'))

参考资料

  • 教程类

    • 《JavaScript 正则表达式迷你书》

      • JS 正则表达式完整教程 - 老姚
    • 这次不会说我的正则教程没写全了吧??
  • 文档类

    • 正则表达式简明参考
    • 正则表达式
    • 不包含某个字符
    • 至少包含某个字符
    • 最全的常用正则表达式大全——包括校验数字、字符、一些特殊的需求等等
  • 实用?

    • 网址校验

      • 验证类
      • JavaScript 正则编写 - 原理篇
  • 工具

    • regexper: 正则可视化网站

  1. 0-9 ↩
  2. 0-9a-zA-Z_ ↩
  3. tvnrf ↩
  4. nru2028u2029 ↩

正文完
 0