前端正则表达式 — RegExp

前言

作为一名程序员,你肯定是知道正则表达式的。作为计算机领域最伟大的发明之一,正则表达式简单、强大,它可以极大地提高我们工作中的文本处理效率。现在各大操作系统、编程语言、文本编辑器也都已经支持正则表达式。

但经常在⽹上看到许多⼈抱怨正则表达式【难学】,发现大家和我之前的做法⼀样:⽤到什么功能,就去⽹上搜⼀个例⼦来改改,能跑通就满意。⾄于这例⼦到底如何构成的,⾃⼰是不是都懂了,其实⼼⾥没底,能⼤概看懂五六分,就已经很满⾜了。

但是这样治标不治本。有点像挖井,每次挖到⼀点⽔就满⾜了,根本不关⼼挖没挖到含⽔层。结果就是每次要喝⽔的时候,你都得重新打⼀眼井。那么对于正则表达式,我们有没有可能打出⼀⼝【永不⼲涸的深井】?当然有,那就需要 ⼀次性多投⼊点时间,由表及里,由术及道。一旦掌握了方法,之后就会简单很多了

如果每天花一刻钟,坚持一个礼拜,从了解到熟悉,从熟悉到理解,达到【不忘】的阶段。多投⼊时间很好理解,但什么叫掌握⽅法呢?深⼊正则表达式概念思维层⾯。不要盯着正则字符和表达式皱眉,⽽要把真正的【规律】给找出来。也正因为这样,我们才需要⼀次性多投⼊点时间。

概述/简介

正则表达式,英文是 Regular Expression,简称RE。顾名思义,正则其实就是一种描述 文本内容组成规律的表示方式(pattern)。

简单来说,正则是一个非常强大的文本处理工具,它的应用极其广泛。我们可以利用它来校验数据的有效性,比如表单输入是不是符合要求;也可以从文本中提取想要的内容,比如从网页中抽取数据;还可以用来做文本内容替换,从而得到我们想要的内容。

而在前端 Javascript 中,正则对象是 RegExp 。 它通常应用于 RegExp 对象的 exectest方法,以及 String 对象的 matchmatchAllreplacesearchsplit 等方法中。

那么接下来让我们一起学习前端正则表达式~

初识正则表达式

RegExp 声明定义

正则表达式定义有两种方式:

  • 一种是使用 字面量 ,以斜杠表示开始和结束
  • 一种是使用 RegExp 构造函数,分别接收两个参数:正则表达式/文本、修饰符
js 复制代码
  // 字面量 定义
  const reg1 = /abc/i
  
  // RegExp 构造
  const reg2 = new RegExp('abc', 'i')
  const reg3 = new RegExp(/abc/, 'i')
  

上面写法是等价的。它们主要区别是第一种方法在引擎编译代码时,就会新建正则表达式,第二种方法在运行时新建正则表达式,所以前者的效率较高。而且前者比较便利和直观。在实际应用中,基本上都采用字面量定义正则表达式。不过在后者在动态生成正则表达式中更具独特的优势。

RegExp 实例属性

正则对象的实例属性分成两类

  • 一类是与修饰符相关,用于了解设置了什么修饰符(后面会更详细的说明)

    • RegExp.prototype.flags:返回已经设置的所有修饰符
    • RegExp.prototype.dotAll:表示设置了s修饰符,其意是.允许匹配换行符
    • RegExp.prototype.sticky:表示设置了y修饰符, 其意搜索是否是粘黏
    • RegExp.prototype.global:表示设置了g修饰符, 其意是否全局查找匹配
    • RegExp.prototype.unicode:表示设置了u修饰符,识别 Unicode 代码点
    • RegExp.prototype.multiline:表示设置了m修饰符,其意是否多行搜索
    • RegExp.prototype.ignoreCase:表示设置了i修饰符, 其意是否忽略大小写
    • RegExp.prototype.hasIndices:表示设置了d修饰符,为每个捕获组子字符串提供索引
    js 复制代码
      (/foobar/gimsy).flags // gimsy
  • 一类是与修饰符无关属性

    • RegExp.prototype.lastIndex

      返回整数,表示下一次开始搜索的位置。该属性可读写,但只在进行连续搜索时有意义(如上个例子中设置了修饰符g或y时)

    • RegExp.prototype.source

      返回正则表达式的字符串形式(不包括反斜杠),该属性只读。

    js 复制代码
      (/\w\d/g).lastIndex // 0
      (/\w\d/g).source // '\\w\\d'

RegExp 实例方法

  • RegExp.prototype.test()

    返回布尔值,表示当前模式是否能匹配参数字符串。如果正则表达式带有gy 修饰符时,则每次都从上一次结束的位置开始向后匹配。但是如果未匹配,则lastIndex属性重置为0

    js 复制代码
     /^cats/.test('cats and dogs') // true
     /^dogs/.test('cats and dogs') // false
  • RegExp.prototype.exec()

    返回匹配结果,如果未发现匹配返回null,如果匹配则返回一个数组,成员是匹配的子字符串,此外返回的数组还包含以下属性

    • input:整个原字符串。
    • index:模式匹配成功的开始位置(从0开始计数)
    • groups:如果有命名分组,则返回一个对象 { [分组名称]: 匹配字符串 },否则 undefined

    其他说明:

    1. 如果正则表示式包含圆括号(即含有"组匹配"),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的length属性等于组匹配的数量再加1。

    2. 如果正则表达式加上 gy 修饰符,则可以使用多次exec()方法,下一次搜索的位置从上一次匹配成功结束的位置开始。但是如果未匹配,则lastIndex属性重置为0。这个模式与test()方法一致。

js 复制代码
  const str = '_aa_aba_abba_'
  const reg = /a(?<name>b+)?a/g
  
  reg.exec(str) 
  /** 执行结果
   * {
   *   0: "aa"
   *   1: undefined
   *   groups: {name: undefined}
   *   index: 1
   *   input: "_aa_aba_abba_"
   *   length: 2
   * }
   */
   
   
  reg.exec(str) 
  /** 执行结果
   * {
   *   0: "aba"
   *   1: "b"
   *   groups: {name: "b"}
   *   index: 4
   *   input: "_aa_aba_abba_"
   *   length: 2
   * }
   */
   
   
  reg.exec(str) 
  /** 执行结果
   * {
   *   0: "aba"
   *   1: "bb"
   *   groups: {name: "bb"}
   *   index: 8
   *   input: "_aa_aba_abba_"
   *   length: 2
   * }
   */
   
   
  reg.exec(str) 
  /** 执行结果
   * null
   */

String 实例方法

  • String.prototype.match()

    字符串的match方法与正则对象的exec方法类似,匹配成功返回一个数组,匹配失败返回null。但是如果正则表达式带有g修饰符,则该方法与正则对象的exec方法行为不同,会一次性返回所有匹配成功的结果。

    js 复制代码
      const str = '_aa_aba_abba_'
      const reg1 = /a(?<name>b+)?a/
      const reg2 = /a(?<name>b+)?a/g
    
      str.match(reg1) 
      /** 执行结果
       * {
       *   0: "aa"
       *   1: undefined
       *   groups: {name: undefined}
       *   index: 1
       *   input: "_aa_aba_abba_"
       *   length: 2
       * }
       */
       
      
      str.match(reg2) 
      /** 执行结果
       * ['aa', 'aba', 'abba']
       */
  • String.prototype.split()

    字符串对象的split方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。该方法接受两个参数,第一个参数是正则表达式表示分隔规则,第二个参数是返回数组的最大成员数。

    js 复制代码
      ('a-aa-aaa').split(/-/) // ['a', 'aa', 'aaa']
      ('a-aa-aaa').split(/-/, 2) // ['a', 'aa']
      
      // 如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回。
      ('a-aa-aaa').split(/(-)/) // ['a', '-', 'aa', '-', 'aaa']
      ('a-aa-aaa').split(/(-)/, 2) // ['a', '-']
  • String.prototype.search()

    字符串对象的search方法,返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1

    js 复制代码
      ('a-aa-aaa').search(/aa/) // 2
      ('a-aa-aaa').search(/aaaa/) // -1
  • String.prototype.replace()

    字符串对象的replace方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容。

    • 正则表达式如果不加g修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。

      js 复制代码
        'a-aa-aaa'.replace('a', 'b') // "b-aa-aaa"
        'a-aa-aaa'.replace(/a/, 'b') // "b-aa-aaa"
        'a-aa-aaa'.replace(/a/g, 'b') // "b-bb-bbb"
    • replace方法的第二个参数可以使用美元符号$,用来指代所替换的内容。

      • $&:匹配的子字符串
      • ``$```:匹配结果前面的文本
      • $':匹配结果后面的文本
      • $n:匹配成功的第n组内容,n是从1开始的自然数, 支持 1~99
      • $$:指代美元符号$
      js 复制代码
        '2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, '$&') // "2023-09-05/hello world - lin"
        '2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, '$`') // "2023-09-05/2023-09-05/ - lin"
        '2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, "$'") // "2023-09-05/ - lin - lin"
        '2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, "$1") // "2023-09-05/hello - lin"
        '2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, "$$") // "2023-09-05/$ - lin"
    • replace方法第二个参数还可以是一个函数,将每个匹配内容替换为函数返回值。

      作为一个替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应参数)另外最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置,最后一个参数是原字符串。如果正则表达式存在命名分组,则最后一个参数原字符串后面还会再多个命名分组匹配的参数

      js 复制代码
       '2023-09-05/hello world - lin'.replace(/([a-z]+)\s([a-z]+)/, function($0, $1, $2, $3, $4) {
          console.log(`$0: ${$0}`)
          console.log(`$1: ${$1}`)
          console.log(`$2: ${$2}`)
          console.log(`$3: ${$3}`)
          console.log(`$4: ${$4}`)
          return $0
       })
          
       //
       // console.log: 
       //   $0: hello world
       //   $1: hello
       //   $2: world
       //   $3: 11
       //   $4: 2023-09-05/hello world - lin
       //

了解正则表达式

在初识正则表达式中,我们已经熟悉了在前端 Javascript 中正则表达式的声明和定义、正则表达式的实例属性/方法、正则表达式不同修饰符各自作用,以及通过 RegExp/String 对象的实例方法的进行使用。那么接下来,我们逐步梳理并了解正则表达式中的术语和概念(字面量字符元字符预定义模式量词符修饰符等),希望大家对正则表达式基础知识和概念有更深的了解。

字面量字符

大部分字符在正则表达式中,就是字面的含义,比如 /a/ 匹配 a/b/ 匹配 b。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的 ab),那么它们就叫做"字面量字符"(literal characters)。

预定义模式

预定义模式指的是某些常见模式的简写方式。

  • \d 匹配0-9之间的任一数字,相当于[0-9]
  • \D 匹配所有0-9以外的字符,相当于[^0-9]
  • \w 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]
  • \W 除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9_]
  • \s 匹配空格(包括换行符、制表符、空格符等),相等于[ \t\r\n\v\f]
  • \S 匹配非空格的字符,相当于[^ \t\r\n\v\f]
  • \b 匹配词的边界。
  • \B 匹配非词边界,即在词的内部。
js 复制代码
// \b 的例子
/\bworld/.test('hello world') // true
/\bworld/.test('hello-world') // true
/\bworld/.test('helloworld') // false

// \B 的例子
/\Bworld/.test('hello world') // false
/\Bworld/.test('hello-world') // false
/\Bworld/.test('helloworld') // true

元字符

除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做"元字符"(metacharacters)

  • 点字符 .

    匹配除回车(\r)、换行(\n) 、行分隔符(\u2028)和段分隔符(\u2029)以外的所有字符

  • 位置字符 ^$

    分别表示字符串的开始位置和字符串的结束位置。当设置了 multiline 修饰符时,^$ 还会匹配每行行首和行尾

  • 选择符 |

    在正则表达式中表示"或关系"(OR),即cat|dog表示匹配catdog

  • 字符类

    表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如[xyz] 表示xyz之中任选一个匹配。其中有两个字符在字符类中有特殊含义:

    • 脱字符(^)

      如果方括号内的第一个字符是[^],则表示除了字符类之中的字符,其他字符都可以匹配。比如,[^xyz]表示除了xyz之外都可以匹配

    • 连字符(-)

      某些情况下,对于连续序列的字符,连字符(-)用来提供简写形式,表示字符的连续范围。比如,[abc]可以写成[a-c][0123456789]可以写成[0-9],同理[A-Z]表示26个大写字母。

    js 复制代码
      /^[abc]/.test('world') // false
      /^[abc]/.test('apple') // true
      
      /^[^abc]/.test('world') // true
      /^[^abc]/.test('apple') // false
      
      /a-z/.test('b') // false
      /[a-z]/.test('b') // true
  • 转义符

    正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配 +,就要写成 \+。正则表达式中需要反斜杠转义的,一共有12个字符:^.[$()|*+?{\。需要特别注意的是,如果使用RegExp方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。

    js 复制代码
      /1+2/.test('1+2') // false
      /1\+2/.test('1+2') // true
      
      (new RegExp('1\+2')).test('1+2') // false
      (new RegExp('1\\+2')).test('1+2') // true
  • 特殊字符

    正则表达式对一些不能打印的特殊字符,提供了表达方法。

    • \n 匹配换行键。
    • \r 匹配回车键。
    • \t 匹配制表符 tab(U+0009)。
    • \v 匹配垂直制表符(U+000B)。
    • \f 匹配换页符(U+000C)。
    • \xhh 匹配一个以两位十六进制数(\x00-\xFF)表示的字符。
    • \uhhhh 匹配一个以四位十六进制数(\u0000-\uFFFF)表示的 Unicode 字符。

量词符

量词符用来设定某个模式出现的次数。精确匹配次数时,使用大括号({})表示。{n}表示恰好重复n次,{n,}表示至少重复n次,{n,m}表示重复不少于n次,不多于m次。

  • ? 问号表示某个模式出现0次或1次,等同于 {0,1}
  • * 星号表示某个模式出现0次或多次,等同于 {0,}
  • + 加号表示某个模式出现1次或多次,等同于 {1,}
js 复制代码
  /^\d{3}$/.test('123') // true
  /^\d{3}$/.test('1234') // false

  /^\d{3,}$/.test('123') // true
  /^\d{3,}$/.test('1234') // true
  
  /^\d{2,3}$/.test('123') // true
  /^\d{2,3}$/.test('1234') // false

修饰符

  • RegExp.prototype.dotAll

    表示设置了s修饰符,其作用是特殊字符 . 应匹配字符串中的下述行终结符

    • U+000A 换行符"\n")
    • U+000D 回车符("\r")
    • U+2028 行分隔符(line separator)
    • U+2029 段分隔符(paragraph separator)
    js 复制代码
      (/foo.bar/).dotAll // false
      (/foo.bar/s).dotAll // true
      (/foo.bar/).test('foo\nbar') // false
      (/foo.bar/s).test('foo\nbar') // true
  • RegExp.prototype.global

    表示设置了g修饰符,其意是全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。

    js 复制代码
      (/foo\w+/).global // false
      (/foo\w+/g).global // true
      ('foobar\nfootball').match(/foo\w+/) // ['foobar']
      ('foobar\nfootball').match(/foo\w+/g) // ['foobar', 'football']
  • RegExp.prototype.multiline

    表示设置了 m 修饰符,其作用 ^$ 会匹配每行行首和行尾,即 ^$ 会识别换行符\n

    js 复制代码
      (/^football/).multiline // false
      (/^football/m).multiline // true
      (/^football/).test('basketball\nfootball') // false
      (/^football/m).test('basketball\nfootball') // true
  • RegExp.prototype.ignoreCase

    表示设置了i修饰符, 意味着在字符串进行匹配时,应该忽略大小写

    js 复制代码
      (/^football/).ignoreCase // false
      (/^football/i).ignoreCase // true
      (/^football/).test('Football') // false
      (/^football/i).test('Football') // true
  • RegExp.prototype.unicode

    表示设置了u修饰符,开启了多种 Unicode 相关的特性,任何 Unicode 代码点的转义都会被解释。

    js 复制代码
      (/\u{61}/).unicode // false
      (/\u{61}/u).unicode // true
      (/\u{61}/).test('a') // false
      (/\u{61}/u).test('a') // true
  • RegExp.prototype.sticky

    表示设置了y修饰符, 仅从正则表达式的 lastIndex 属性表示的索引处为目标字符串匹配,并且不会尝试从后续索引匹配。如果表达式同时指定了 stickyglobal,其将会忽略 global 标志。

    js 复制代码
      /**
       *  a) 仅从正则 lastIndex 属性指向的索引为目标字符串开始进行匹配(/foo/y 感觉更像 /^foo/y)
       *  b) 一个表达式同时指定了 `sticky` 和 `global`,将会忽略 `global` 标志
       *  c) 从下面例子注意 sticky 和 global 修饰符的区别
       */
      const str1 = 'foobarfootball'
      const reg1 = /foo/y
      const reg2 = /^foo/g
    
      // ------- sticky: reg1 ---------
      reg1.lastIndex // 0
      reg1.test(str1) // true
      reg1.lastIndex // 3
    
      reg1.lastIndex // 3
      reg1.test(str1) // false, 因为索引指向第3个字符, /^foo/y.test('barfootball')
      reg1.lastIndex // 0, 因为没匹配,所有重置 reg1.lastIndex 为 0
    
      reg1.lastIndex = 6 // 设置 lastIndex 索引为 6
      reg1.test(str1) // true, 因为索引指向第6个字符, /^foo/y.test('football')
      reg1.lastIndex // 9
    
    
      // ------- global: reg2 ---------
      reg2.lastIndex // 0
      reg2.test(str1) // true
      reg2.lastIndex // 3
    
      reg2.lastIndex // 3
      reg2.test(str1) // false
      reg2.lastIndex // 0, 因为没匹配,所有重置 reg2.lastIndex 为 0
    
      reg2.lastIndex = 6 // 设置 lastIndex 索引为 6
      reg2.test(str1) // false, 注意与sticky的区别,'g'修饰的正则表达式中^只能匹配全文的开头字符
      reg2.lastIndex // 0, 因为没匹配,所有重置 reg2.lastIndex 为 0

深入正则表达式

在上一个小节中,我们一起熟悉了正则表示式的一些术语和概念,对正则的基础知识有了进一步的了解。那在这个小节中,我们一起学习正则表达式中 分组匹配(分组匹配捕获分组引用命名捕获分组非捕获分组零宽断言 )和正则匹配模式(贪婪模式非贪婪模式独占模式)。正是部分使得正则表达式晦涩难懂,也正是这些使得正则表达式高效而又强大。

分组匹配

分组和编号

括号在正则中可以 用于分组 ,被括号括起来的部分 「子表达式」会被保存成一个 子组。那分组和编号的规则是怎样的呢?其实很简单,第几个括号就是第几个分组。那如果括号存在嵌套括号呢,也很简单,可以数左括号计数,哈哈。

例如 这里有个日期 2023-09-06 12:00:01,我们使用正则匹配日期和时间来验证下分组规则。

js 复制代码
  const date = '2023-09-06 12:00:01'
  const regex = /((\d{4})-(\d{2})-(\d{2})) ((\d{2}):(\d{2}):(\d{2}))/
    
  date.replace(regex, ($0, $1, $2, $3, $4, $5, $6, $7, $8) => {
    console.log(`$0: ${$0}`) // 匹配的文本
    console.log(`$1: ${$1}`) // 第一个分组捕获
    console.log(`$2: ${$2}`) // 第二个分组捕获
    console.log(`$3: ${$3}`) // 第三个分组捕获
    console.log(`$4: ${$4}`) // 第四个分组捕获
    console.log(`$5: ${$5}`) // 第五个分组捕获
    console.log(`$6: ${$6}`) // 第六个分组捕获
    console.log(`$7: ${$7}`) // 第七个分组捕获
    console.log(`$8: ${$8}`) // 第八个分组捕获
    return $0
  })
        
  //
  // console.log: 
  //   $0: 2023-09-06 12:00:01
  //   $1: 2023-09-06
  //   $2: 2023
  //   $3: 09
  //   $4: 06
  //   $5: 12:00:01
  //   $6: 12
  //   $7: 00
  //   $8: 01
  //

命名捕获分组

前面我们讲了分组编号,但由于编号得数在第几个位置,后续如果发现正则有问题,改动了括号的个数,可能导致编号发生变化 ,因此提供了 命名分组(named grouping) ,这样和数字相比更容易辨识,不容易出错。命名分组的格式为 (?<分组名>...)

还是这个日期例子 2023-09-06 12:00:01,我们使用正则匹配日期和时间来验证下命名分组。

js 复制代码
  const date = '2023-09-06 12:00:01'
  const regex = /(?<date>(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})) (?<time>(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2}))/
    
    
  date.replace(regex, '$<year>年$<month>月$<day>日 $<hour>点$<minute>分$<second>秒')
  // result: 
  //   '2023年09月06日 12点00分01秒'
    
    
  date.replace(regex, ($0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) => {
    console.log(`$0: ${$0}`) // 匹配的文本
    console.log(`$1: ${$1}`) // 第一个分组捕获
    console.log(`$2: ${$2}`) // 第二个分组捕获
    console.log(`$3: ${$3}`) // 第三个分组捕获
    console.log(`$4: ${$4}`) // 第四个分组捕获
    console.log(`$5: ${$5}`) // 第五个分组捕获
    console.log(`$6: ${$6}`) // 第六个分组捕获
    console.log(`$7: ${$7}`) // 第七个分组捕获
    console.log(`$8: ${$8}`) // 第八个分组捕获
    console.log(`$9: ${$9}`) // 匹配位置索引
    console.log(`$10: ${$10}`) // 整个文本
    console.log(`$11:`, $11) // 命名分组匹配对象
    return $0
  })
        
  //
  // console.log: 
  //   $0: 2023-09-06 12:00:01
  //   $1: 2023-09-06
  //   $2: 2023
  //   $3: 09
  //   $4: 06
  //   $5: 12:00:01
  //   $6: 12
  //   $7: 00
  //   $8: 01
  //   $9: 0
  //   $10: 2023-09-06 12:00:01
  //   $11: { 
  //     date: "2023-09-06", 
  //     year: "2023", 
  //     month: "09", 
  //     day: "06", 
  //     time: "12:00:01", 
  //     hour: "12", 
  //     minute: "00", 
  //     second: "01", 
  //   }
  // 

编号捕获引用

刚刚我们在 String.prototype.replace() 方法中,通过$1$2 的方式引用了正则表达式中的分组匹配项,对于命名分组的,replace 第二个参数作为函数,其函数参数最后一个参数也返回了命名分组匹配的结果。

那在正则表达式内部是否可以引用分组捕获的匹配项呢?答案是可以的。在知道了分组引用的编号(number)后,我们就可以使用 反斜扛 + 编号,即 \number 的方式来进行引用

js 复制代码
  const reg = /^<(\w+)>.+(<\/\1>)$/
  const html = `<html><header></header><body></body></html>`

  html.replace(reg, '$1') // 'html'
  html.replace(reg, '$2') // '</html>'

命名捕获引用

那如果是命名分组匹配 (?<name>), 那么在正则表达式内部通过 \k<name> 引用,详见如下

js 复制代码
  const reg = /^<(?<tag>\w+)>.+(<\/\k<tag>>)$/
  const html = `<html><header></header><body></body></html>`

  html.replace(reg, '$<tag>') // 'html'
  html.replace(reg, '$2') // '</html>'

非捕获分组

在括号里面的会保存成子组 ,但有些情况下,你可能只想用括号将某些部分看成一个整体,后续不用再用它 ,类似这种情况,在实际使用时,是没必要保存子组的。这时我们可以在括号里面使用 (?:) 不保存子组。

js 复制代码
  const reg = /^<(?:\w+)>.+(<\/html>)$/
  const html = `<html><header></header><body></body></html>`

  html.replace(reg, '$1') // '</html>', 因为 (?:\w+) 不捕获,所以 $1 指向 (<\/html>)

零宽断言

在使用正则表达式时,有时我们需要捕获的内容前后必须是特定内容,但又不捕获这些特定内容的时候,零宽断言就起到作用了。零宽断言正如它的名字一样,是一种零宽度的匹配,简单理解,零宽断言匹配不会改变 lastIndex 的索引值。

零宽断言有正向(positive)、负向(negative)、先行(lookahead)、后行(lookbehind),一共有4种组合形式:

零宽断言类型 零宽断言表达式 零宽断言描述 零宽断言标签
零宽正向先行断言 (?=exp) 匹配某位置,该位置后面内容应匹配表达式 exp,【JS支持】 lookahead positive
零宽负向先行断言 (?!exp) 匹配某位置,该位置后面内容不匹配表达式 exp,【JS支持】 lookahead negative
零宽正向后行断言 (?<=exp) 匹配某位置,该位置前面内容应匹配表达式 exp,【JS支持】 lookbehind positive
零宽负向后行断言 (?<!exp) 匹配某位置,该位置前面内容不匹配表达式 exp,【JS支持】 lookbehind negative
  • 先行(lookahead)/ 后行(lookbehind)

    先行断言,引擎会尝试匹配指针还未扫过的字符,先于指针到达该字符,故称为先行。

    后行断言,引擎会尝试匹配指针已扫过的字符,后于指针到达该字符,故称为后行。

  • 正向(positive)/ 负向(negative)

    正向就表示匹配括号中的表达式,负向表示不匹配。

js 复制代码
  // 零宽正向先行断言
  (/Hello (?=World)/).test('Hello World') // true
  (/Hello (?=World)/).test('Hello RegExp') // false
  
  // 零宽负向先行断言
  (/Hello (?!World)/).test('Hello World') // false
  (/Hello (?!World)/).test('Hello RegExp') // true
  
  // 零宽正向后行断言
  (/(?<=Hello) RegExp/).test('Hello RegExp') // true
  (/(?<=Hello) RegExp/).test('Welcome RegExp') // false
  
  // 零宽负向后行断言
  (/(?<!Hello) RegExp/).test('Hello RegExp') // false
  (/(?<!Hello) RegExp/).test('Welcome RegExp') // true

匹配模式

这一节我们讲一下正则中的三种模式,贪婪匹配、非贪婪匹配和独占模式(JS不支持)。

这些模式会改变正则中量词的匹配行为,比如匹配一到多次;在匹配的时候,匹配长度是尽可能长还是要尽可能短呢? 如果不知道贪婪和非贪婪匹配模式,我们写的正则很可能是错误的,这样匹配就达不到期望的效果。

由于本节内容和量词相关的元字符密切相关,所以我们先来回顾一下正则表达式中表示量词的元字符

贪婪匹配(Greedy)

我们来看一下贪婪匹配。在正则中,表示次数的量词 默认是贪婪的,在贪婪模式下,会尝试尽可能最大长度去匹配。

首先,我们来看一下在字符串 aaabb 中使用正则 a* 的匹配过程。

字符串 aaabb
下标 012345
匹配 开始 结束 说明 匹配内容
第 1 次 0 3 到第一个字母b发现不满足,输出 aaa aaa
第 2 次 3 3 匹配剩下b发现匹配不上,输出空字符串 空字符串
第 3 次 4 4 匹配剩下b发现匹配不上,输出空字符串 空字符串
第 4 次 5 5 匹配剩下空字符串,输出空字符串 空字符串

非贪婪匹配(Lazy)

正则表达式默认是贪婪的,那么如何将贪婪模式变成非贪婪模式呢?我们可以 在量词后面加上英文的问号 (?) 即可,正则就变成了 a*?

对比 /'.+'//'.+?'/ 的区别

js 复制代码
  ("'Hello Tom', 'Welcome to China'").match(/'.+'/) // ["'Hello Tom', 'Welcome to China'"]
  ("'Hello Tom', 'Welcome to China'").match(/'.+?'/) // ["'Hello Tom']

独占模式匹配(JS不支持

独占模式虽然在 Javascript 中不支持,但对理解贪婪模式和非贪婪模式匹配却很有帮助,所以也在这里进行讲解。

不管是贪婪模式,还是非贪婪模式,都需要发生回溯才能完成相应的功能。但是在一些场景下,我们不需要回溯,匹配不上返回失败就好了,因此正则中还有另外一种模式,独占模式,它类似贪婪匹配,但匹配过程不会发生回溯,因此在一些场合下性能会更好。

你可能会问,那什么是回溯呢?我们来看一些例子

  • 例如下面贪婪模式下的正则:

    js 复制代码
      regex = "xy{1,3}z"
      text = "xyyz"

    在匹配时,y{1,3} 会尽可能长地去匹配,当匹配完 xyy 后,由于 y 要尽可能匹配最长,即三个,但字符串中后面是个 z 就会导致匹配不上,这时候正则就会 向前回溯,吐出当前字符 z,接着用正则中的 z 去匹配。

  • 如果我们把这个正则改成 非贪婪模式

    js 复制代码
      regex = "xy{1,3}?z"
      text = "xyyz"

    由于 y{1,3}? 代表匹配 1 到 3 个 y,尽可能少地匹配 。匹配上一个 y 之后,也就是在匹配上 text 中的 xy 后,正则会使用 z 和 text 中的 xy 后面的 y 比较,发现正则 z 和 y 不匹配,这时正则就会 向前回溯,重新查看 y 匹配两个的情况,匹配上正则中的 xyy,然后再用 z 去匹配 text 中的 z,匹配成功。

我们比对理解了贪婪模式非贪婪模式 ,那么如何理解独占模式呢?它和贪婪模式很像,独占模式会尽可能多地去匹配,但如果匹配失败就结束,不会进行回溯,这样的话就比较节省时间。

自测/实践

到这里我们已经全面梳理并学习了正则表达式基础知识和相关API的应用。下面我列举一些需要正则表达式实现的例子,感兴趣的朋友们可以尝试实践下:

转换日期格式

js 复制代码
  // 原日期 2023-09-06 15:15:28  
  //
  //  a) 转换成 2023年09月06日 15时15分秒
  // 

提取 div标签

js 复制代码
  // 内容: '<html><head></head><body><div><div>innner content</div></div></body></html>'
  // 
  //  a) 提取: '<div><div>innner content</div></div>'
  //  b) 提取: '<div>innner content</div>'
  //  c) 提取: 'innner content'
  //

转换 glob 语法

glob 语法转换成 RegExp 对象

glob 通配符 glob 描述 例子 匹配 不匹配
* 匹配任意数量的任何字符,包括无 Law* Law,Laws,Lawyer GrokLaw,La,aw
? 匹配任何 单个 字符 ?at Cat,cat,Bat,bat at
[abc] 匹配括号中给出的一个字符 [CB]at Cat,Bat cat,bat
[a-z] 匹配括号中给出的范围中的一个字符 Letter[0-9] Letter0,Letter1...Letter9 Letters,Letter,Letter10
[!abc] 匹配括号中未给出的一个字符 [!C]at Bat,bat,cat Cat
[!a-z] 匹配不在括号内给定范围内的一个字符 Letter[!3-5] Letter1... Letter3...Letter5, Letterxx
{a..z} 匹配括号中给出的一个字符,等同于[abc] {CB}at Cat,Bat cat,bat
{start..end} 会匹配连续范围的字符 d{a..d}g dag,dbg,dcg,ddg
相关推荐
前端啊龙3 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠7 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds27 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓3 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4113 小时前
无网络安装ionic和运行
前端·npm
理想不理想v3 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试