前端正则表达式 — 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
相关推荐
BBB努力学习程序设计19 小时前
CSS Sprite技术:用“雪碧图”提升网站性能的魔法
前端·html
BBB努力学习程序设计19 小时前
CSS3渐变:用代码描绘色彩的流动之美
前端·html
冰暮流星19 小时前
css之动画
前端·css
jump68020 小时前
axios
前端
spionbo20 小时前
前端解构赋值避坑指南基础到高阶深度解析技巧
前端
用户40993225021220 小时前
Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗
前端·ai编程·trae
开发者小天20 小时前
React中的componentWillUnmount 使用
前端·javascript·vue.js·react.js
永远的个初学者21 小时前
图片优化 上传图片压缩 npm包支持vue(react)框架开源插件 支持在线与本地
前端·vue.js·react.js
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ21 小时前
npm i / npm install 卡死不动解决方法
前端·npm·node.js
Kratzdisteln21 小时前
【Cursor _RubicsCube Diary 1】Node.js;npm;Vite
前端·npm·node.js