JS正则表达式完整梳理

952 阅读11分钟

RegExp对象

JavaScript中通过内置对象RegExp支持正则表达式,一般地,我们有两种方法来实例化RegExp对象。

字面量

var expression = /pattern/flags

其中,pattern为正则表达式。flags为修饰符,可以支持一个或多个。

构造函数

在ES5中,RegExp构造函数的参数有两种情况。

var reg1 = new RegExp('abc', 'g') // 第一个参数为字符串,第二个为修饰符
var reg2 = new RegExp(/abc/g) // 参数为一个正则表达式

其中的g是一个修饰符,表示全局匹配,找到所有满足条件的组合。上述两种书写是等价的。

在ES6中,支持在第一个参数为正则表达式的情况下,第二个参数接受修饰符。此时,会忽略第一个参数中原有的修饰符。

var reg3 = new RegExp(/abc/, g)
var reg4 = new RegExp(/abc/g, i) // 忽略正则表示的修饰符,使用第二个参数作为修饰符。等价于 var reg4 = /abc/i

元字符

字符 | 释义

  • | - \d | 匹配一个数字 \D | 匹配一个非数字字符 \w | 匹配一个单字字符(字母、数字或下划线) \W | 匹配一个非单字字符 \s | 匹配一个空格字符(空格、制表符、换页符和换行符) \S | 匹配一个非空格字符 \b | 单词边界 . | 匹配除换行符外的任意单个字符 通过观察,我们可以看出大写表示小写的反义,所以组合/\d\D/、/\w\W/、/\s\S/可以匹配任意字符。

来看一个例子,假如我们想要匹配三个连续单字,我们首先会想到的是/\w\w\w/

var str = 'bad very apple bear eat module'
var reg = /\w\w\w/g
str.match(reg)
// ["bad", "ver", "app", "bea", "eat", "mod", "ule"]

正则表达式实际上匹配一个连续串的规则,因此能够匹配到3个字母的单词,也可以匹配6个字母的单词(module)。

那么这时候我们想要的是匹配三个字母组成的单词呢?这时候需要用到匹配单词边界\b

// 修改正则表达式
var reg = /\b\w\w\w\b/g

// 结果
// ["bad", "eat"]

从这里我们可以看出,单词边界指的是一个词不被前或后的另一个字符"跟随"的位置,比如字母和空格之间。而匹配的结果不包含字边界,意味着其长度为0。例如/\ba/可以匹配"apple"的"a",/pp\b/不匹配"apple"的"pp",因为pp后跟随着le。

这里我们可以具体把\b看作是\w\W之间的位置,还包括与首尾之间的位置。


var str = 'everglow_01.mp4'
var result = str.replace(/\b/g, #)
console.log(result)
// #everglow_01#.#mp4#

转义

正则表达式中可以通过\来转义,还原字符本身的含义。例如我们知道.可以匹配除换行符之外的任意字符,当我们想要匹配.本身的时候,我们可以通过\.来还原.本身作为一个点的含义。

如果在非特殊含义的字符前加上\不会有影响。

字符组

当我们想要匹配一个不确定的字符时,我们需要通过字符组来匹配。通过使用[]方括号,其中列出所有的可选项,它们之间关系为"或",最终匹配出一个字符。

例如,我们通过/[ab]c/可以匹配abac。方括号中匹配的内容为a或b。

转义\

特殊字符在[]内不需要进行转义,即[.]表示匹配一个点。

连字符 -

通常我们使用字符组的时候是需要针对一个范围进行匹配,连字符-的意思是匹配的范围是它左边和右边之内的值。

var str = 'b3 46 c5'
var reg = /[a-z]\d/g
str.match(reg)
// ["b3", "c5"]

上述的正则表达式,表示匹配一个字母加一个数字的组合,其中字母的范围是从a到z。

我们还可以同时匹配多个范围,例如/[a-zA-Z]/,这样表示匹配小写的a-z与大写的A-Z范围内的字母。

如果单独使用,或者不符合一个范围使用,则仍然表示匹配一个-。例如/[-c]/表示匹配-或者c

取反 ^

[]使用^表示取反。

var str = 'b2 a2 c9'
var reg = /[^ab]\d/g
str.match(reg)
// ["c9"]

可以看出,取反的意义是匹配除a和b之外的字符。

位置(边界)

字符释义
^匹配文本的开始
$匹配文本的结束

上面提到在[]内使用^表示中括号内的取反,正常使用的^,则表示匹配输入文本的开始。

var str = 'real react'
var reg = /^r\w+/g
str.match(reg)
// ["real"]

这里只会匹配到real,因为^匹配的是文本的开头。注意与单词匹配的区分,如果想要匹配 r 开头的单词,可以使用单词边界:/\br\w+\b/

量词

字符释义
?匹配前面一个表达式0次或者1次
  • | 匹配前面一个表达式1次或者多次
  • | 匹配前面一个表达式0次或者多次 {min,max} 匹配前面一个表达式从min到max之间的次数,max为空时表示匹配至少min次 {n} | 匹配前面一个表达式n次

这里的次数表示重复前一个表达式的次数。这里表达式指是一个集合,这个集合可以是单个字符,也可以由若干个字符组成,这个后面会提到。

var str = 'apppple bear'
var reg = /ap{1,4}le/g
str.match(reg)
// ["apppple"]

捕获分组

通过上面量词的例子,我们想要匹配多次出现的p,我们可以使用/p{1,4}/。那么这时候我们如果想要匹配多次出现的字符组合呢?这个时候我们需要用到正则表达式中的分组()

var str = 'appappapp'
var reg = /(app){3}/g
str.match(reg)
// ["appappapp"]

我们通过使用了分组,将app进行包裹,此时可配合量词达到匹配字符组合的目的。

上面提到,我们想要表示“或”的关系可以使用字符组[]。同样地,如果我们想要表示几个字符组合之间的"或"关系时,我们需要用到()


var str = 'react really'
var reg = /\brea(ct|lly)/g
str.match(reg)
// ["react", "really"]

引用

我们在使用()时,实际上产生了一个又一个的捕获分组。而我们可以通过多种方法来获取到某些具体的分组。

$


var str = '123-456-7890'
var reg = /(\d{3})-(\d{3})-(\d{4})/
str.match(reg)
// ["123-456-7890", "123", "456", "7890", index: 0, input: "123-456-7890", groups: undefined]
RegExp.$1
// "123"

通过match方法返回结果来看,除了返回匹配文本自身外,还返回了括号内的捕获分组。并且,我们还能够通过RegExp.$1显式得去获取某个具体的分组。

在JavaScript中,正则匹配成功的字符串可以用+数字的形式表示,+数字的形式表示,`1表示第一个匹配成功的,$2`表示第二个匹配成功的,以此类推。

同时,也可以使用构造函数的全局属性$1$9来获取,$_表示匹配文本。

我们可以在上面例子的基础上,配合replace方法使用。


var result = str.replace(reg, '$3/$2/$1')
console.log(result)
// 7890/456/123

\n

通过使用$n的形式获取分组,我们可以在正则表达式外,使用捕获分组的引用。在正则表达式内,我们可以通过\+ 数字 的形式,它返回最后的第n个子捕获匹配的子字符串。


var str = '@@react@@'
var reg1 = /@@.*@@/
var reg2 = /(@@).*(\1)/
str.match(reg1) // ["@@react@@", index: 0, input: "@@react@@", groups: undefined]
str.match(reg2) // ["@@react@@", "@@", "@@", index: 0, input: "@@react@@", groups: undefined]

上述的例子中,reg2中的(@@)表示第一个分组,\1表示引用第一个分组,即@@,因此reg1与reg2是等价的。

当捕获分组不存在时,例如以上例子不存在第四个分组,那么这时候是进行了\的转义,表示对4这个数字的转义。

如果存在括号嵌套的情况下,我们可以通过判断开括号(的位置来确定分组顺序。

捕获命名

ES2018中,引入了具名组匹配,允许为每一个分组指定一个名字,这样既利于阅读,也方便引用。具体的方法是在括号内最前面添加?<key>,其中,key为我们想要命名的组名。


var result = '2018-09-10'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
console.log(result)

对比于之前的例子,使用捕获命名后,返回结果的groups属性不再是undefined,而是每个组名与对应的匹配值的键值对。

我们可以通过result.groups.xxx获取某个分组的的值。如果命名组没有匹配,则会返回undefined,而这个建名依然会存在于groups中。

忽略分组(非捕获分组)

在某些情况下,我们希望忽略某些分组。这时候我们使用在括号内最前面加上?:


var str = 'Tel:123-456-7890'
var reg = /(?:Tel:)(\d{3})-(\d{3})-(\d{4})/
var result = str.replace(reg, '$1-$2-$3')
console.log(result)
// 123-456-7890

我们可以看出,结果中忽略了(Tel:)分组。

环视(零宽断言)

零宽断言匹配的是:在这个位置之前或者之后,应该有或者没有什么内容。零宽是指它匹配的是一个位置,其本身是没有宽度的。

肯定先行断言

先行指的是向前看(look-ahead),从文本开头向尾部开始解析。断言的这个位置是为前面的规则服务。

语法:圆括号内最前面加上?=


var str = 'JavaScript typescript'
var reg = /\b\w{4}(?=Script\b)/g
str.match(reg)
// ["Java"]

上述的代码中,我们想要匹配的是四个字符,条件是这四个字符必须是一个结尾是Script的单词,这时候显然命中的是JavaScript。这时候返回的结果是Java,而没有返回JavaSciprt,这也正是零宽的特性,它只是去匹配一个位置,而其中的内容不是匹配结果的一部分。

肯定后行断言

后行指的是从后想钱看(look-behind),与先行相反。

语法:圆括号内最前面加上?<=


var str = 'fruitApple softwareAdobe'
var reg = /(?<=\bfruit)A\w+\b/g
str.match(reg)
// ["Java"]

上述代码中,我们想要匹配词组中A开头到词组的结尾的部分,条件是这个词组是以fruit开头,这时候按后行的规则,从尾部往开头解析,命中了fruitApple

否定先行断言

否定的含义与上述肯定相反,相当于一个取反的意义。

语法:圆括号最前面加上?!


var str = 'JavaScript typescript'
var reg = /\b\w{4}(?!Script\b)/g
str.match(reg)
// ["type"]

否定后行断言

语法:圆括号最前面加上?<!


var str = 'fruitApple softwareAdobe'
var reg = /(?<=\bfruit)A\w+\b/g
str.match(reg)
// ["Adobe"]

通过以上的例子可以发现,否定的语法相当于是把肯定中的=替换为!

整数千位分割符的实现

var num = '1234567'
var reg = /\d{1, 3}(?=(\d{3})+$)/g
num.replace(reg, '$&,')
// 1,234,567

贪婪与惰性

贪婪模式是正则中的默认模式,指的是在既定的规则之下匹配尽可能多的文本。


var str = '<div id="container" class="main"></div>'
var reg = /id=".*"/g
str.match(reg)
// ["id="container" class="main""]

从上述代码可以看出,实际上会匹配到main后面的"而不在container后面的"停下。在贪婪模式下,正则会去尽可能的多匹配,在规则范围内,匹配越多越好。例如/\w*//\d+/

与贪婪所对应的就是惰性,顾名思义,惰性模式下会匹配尽可能少的文本。

假设我们只想要匹配id相关的信息,即匹配到container后面的"为止。我们可以使用惰性模式。


var str = '<div id="container" class="main"></div>'
var reg = /id=".*?"/g
str.match(reg)
// ["id="container""]

从上述代码可以看出,通常我们在量词后面加上?就可以实现惰性匹配,例如/\w*?//\d{2,5}?/

我们在上面提到的/(a|b)c/这种或关系的匹配就是惰性的,当匹配到ac时,不会再继续匹配。

回溯

我们在学习正则表达式的时候,或多或少会听到"回溯"这个词。回溯与正则引擎有关,正则引擎分为NFA和DFA。

JavaScript使用的是NFA。其匹配过程是吃进一个字符,如果通过,则再次耻辱;如果不通过,则吐出,回到上一个状态。我们知道在正则中会存在不同状态的路径,例如/(ab|ac)d/,如果一条路径不匹配,则会退回去尝试另一条路;如果通过,则继续吃进字符探索。这个吐出字符、状态的回退过程,就是所谓的"回溯"。

无回溯情况

··· javascript

var str = 'abbbc' var reg = /ab{3}c/g str.match(reg) // ["abbbc"]

···

上述代码的匹配过程是没有触发回溯。从正则表达式的第一个字符a开始,首先吃进abbbc的第一个字符a,二者相同,匹配成功。继续吃进第二个字符b,仍然匹配,以此类推。由于匹配默认是贪婪的,所以每一步都能够匹配成功,一直到最后一个字符。整个过程中没有出现需要"吐出"的回溯。

触发回溯的匹配


var str = 'aaaab'
var reg = /^(a*)ab/
str.match(reg)

  1. 匹配的开始,a*会去尽可能多的捕获a

  2. 随着a*的一直捕获,最终会遇到字符串最后的一个b

  3. 继续执行正则表达式最后的ab进行匹配,而我们的字符串中由于被a*捕获了所有的a,此时仅剩下一个b,那么这个时候显然是无法完成匹配的。

  4. 此时a*从已经捕获的字符中吐出一个a,此时剩余的字符串为ab,被捕获的是aaa

  5. 重新执行正则中的ab进行匹配,发现与剩余的字符串ab匹配,匹配过程结束。

通过上述的步骤,展示了贪婪模式下的匹配过程。我们可以看出3,4步由于暂时无法匹配而吐出字符的过程,就是触发了回溯。

触发形式

上面的例子中,我们可以看出,一般的量词能够触发回溯。因为匹配的过程是贪婪的,所以会尽量先去匹配更多的文本,直到暂时无法匹配时,会不停地去尝试吐出字符再匹配,直到匹配完成。

除了贪婪量词之外,惰性模式下也会存在回溯的情况。


var str = '12345'
var reg = /^(\d{1,3}?)(\d{1,3})$/
str.match(reg)
// ["12345", "12", "345", index: 0, input: "12345", groups: undefined]

上述代码中,我们的目标字符串是12345,第一个分组使用了惰性模式,一开始仅仅匹配了一个1,第二个分组匹配了234,到最后一个字符5发现字符串已经没有剩余,无法进行匹配。这时候就会发生回溯。再给第一个分组塞入2,这时候才能够完成整体的匹配。

性能

我们不能简单从匹配模式来判断性能的好坏,因为贪婪模式与惰性模式都会存在回溯。当回溯越少的情况下,表示性能越好。

通常情况下,由于使用了不合理的正则写法引起灾难性的悲观回溯,可能会导致CPU满载,JavaScript 主进程忙于进行计算,使页面失去响应的情况。例如使用了嵌套量词/(a*)*b/或是前后重复/a*a*/等形式会导致这一情况。

修饰符

g修饰符

全局匹配,单词是global。默认情况下,正则从左向右匹配,只要匹配到了结果就会收工。g修饰符会开启全局匹配模式,找到所有匹配的结果。


var str = 'react really'
var reg1 = /\brea\w+\b/
var reg2 = /\brea\w+\b/g
str.match(reg1) // ["react", index: 0, input: "react really", groups: undefined]
str.match(reg2) // ["react", "really"]

i修饰符

忽略大小写,单词是ignoreCase。通过该修饰符,在匹配时可以忽略字母的大小写。


'JavaScript'.match(/javascript/)
// null

'JavaScript'.match(/javascript/i)
// ["JavaScript", index: 0, input: "JavaScript", groups: undefined]

m修饰符

多行匹配,单词是multiline。通常用于搭配^$。默认情况下^$用于匹配文本的开始与结束,加上该修饰符后,变成了匹配多行文本的开始与结束。


`
abc
xyz
`.match(/^xyz$/);
// null
`
abc
xyz
`.match(/^xyz$/m);
// ["xyz", index: 5, input: "↵abc↵xyz↵", groups: undefined]

y修饰符

粘连,单词是sticky。其同样也是全局匹配,与g不同的是,它的后一次匹配都是从上一次匹配成功的位置开始。而g则是剩余位置中存在匹配项即可。


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

在第二次匹配时,由于y修饰符有位置要求,所以返回了null。

s修饰符

我们知道/./表示匹配除换行符之外的任意字符。当我们使用了s修饰符后,便可以通过.来匹配到包含换行符的任意字符。

u修饰符

含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。由于ES5不支持四个字节的 UTF-16 编码,会将其识别为两个字符,使用u修饰符后,便会识别其为一个字符。

配合JS方法应用

RegExp对象常用的方法

test

RegExp.test(string)

test方法用于判断字符串中是否存在匹配正则表达式模式的字符串,若存在返回true,否则返回false。


var str = 'hello world'
var reg = /hello/
var result = reg.test(str)
console.log(result) // true

exec

RegExp.exec(string)

exec方法在非全局调用时,返回第一个匹配的结果,匹配结果和分组以数组的形式返。


var str = '123-456 456-789'
var reg = /(\d{3})-(\d{3})/
reg.exec(str)
// ["123-456", "123", "456", index: 0, input: "123-456 456-789", groups: undefined]

其中,index是匹配文本的第一个字符的位置,input存放的是被检索的字符串str。如果我们想要返回全部匹配的对象,我们可以使用g修饰符。


var str = '123-456 456-789'
var reg = /(\d{3})-(\d{3})/g
reg.exec(str)
// ["123-456", "123", "456", index: 0, input: "123-456 456-789", groups: undefined]
reg.exec(str)
// ["456-789", "456", "789", index: 8, input: "123-456 456-789", groups: undefined]
reg.exec(str)
// null

从上述代码中看出,我们可以通过反复调用exec()方法来便利字符串中所有匹配的文本,当再也找到匹配文本的时候,便返回null。

支持正则表达式的 String 对象的方法

match

string.match(RegExp)

match方法用于检索字符串是否有符合RegExp匹配的文本,与exec方法类似。

在用作非全局调用的情况下,match方法只匹配一次,如果没有找到文本,则返回null;否则,返回一个数组,结构与exec方法返回结果类似。


var str = '123-456 456-789'
var reg = /(\d{3})-(\d{3})/
str.match(reg)
// ["123-456", "123", "456", index: 0, input: "123-456 456-789", groups: undefined]

match方法与exec方法的区别在于,使用g修饰符的情况下,如果匹配成功,match会以数组的形式返回所有的匹配结果,否则返回null。


var str = '123-456 456-789'
var reg = /(\d{3})-(\d{3})/g
str.match(reg)
// ["123-456", "456-789"]

split

string.split(str|RegExp)

split方法我们通常用于将字符串分割为数组,在某些复杂的情况下,我们可以使用正则表达式来帮助我们分割。


var str = 'a1b2c3d'
var reg = /\d/
var result = str.split(reg)
console.log(result)
// ["a", "b", "c", "d"]

通过上面的代码,我们实现了以数字来分割字符串。

replace

str.replace(RegExp|string, newString|function)

replace方法用于将字符串中的符合条件的若干字符替换为新的字符。其第一个参数除了字符串之后,还接受正则表达式,第二个参数可以是一个函数。


var str = 'Apples and apples are round'
var result = str.replace(/apple/ig, 'oranges')
console.log(result)
// orangess and orangess are round

第一个参数为正则表达式的情况下,第二个参数可以传入一个函数,其返回值作为替换字符串。如果我们使用了g修饰符,那么该函数会被调用多次。我们首先来看这个函数的参数:

变量 | 含义

  • | - match | 匹配的子字符串 p1, p2, p3... | 表示第n个捕获分组,如果没有分组,则该参数不存在 offset | 匹配的子字符串在原字符串中的位置 string | 被匹配的原字符串 string | 被匹配的原字符串 NamedCaptureGroup | 命名捕获组匹配的对象

我们来看一个例子,我们想要将字符串中的@@...@@替换为<div>...</div>


var str = '@@apple@@and@@bear@@'
var result = str.replace(/(@{2})(\w+?)\1/g, function(match, p1, p2) {
  return `<div>${p2}</div>`
})
console.log(result)
// <div>apple</div>and<div>bear</div>