一、RegExp 介绍
ECMAScript
通过RegExp
类型支持正则表达式。正则表达式使用简洁语法来创建:
const expression = /pattern/flags
这个正则表达式的pattern(模式)
可以是任何简单或复杂的正则表达式,包括字符类
、限定符
、分组
、向前查找
和反向引用
。每个正则表达式可以带零个
或多个flags
(标记),用于控制正则表达式的行为。下面给出了表示匹配模式的标记。
-
g
:全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束。 -
i
:不区分大小写,表示在查找匹配时忽略pattern
和字符串的大小写。 -
m
:多行模式,表示查找到一行文本末尾时会继续查找。 -
y
:粘附模式,表示只查找从lastIndex
开始及之后的字符串。 -
u
:Unicode
模式,启用Unicode
匹配。 -
s
:dotAll
模式,表示元字符.
匹配任何字符(包括\n
或\r
)。
使用不同模式和标记可以创建出各种正则表达式,比如:
// 匹配字符串中所有的at
const pattern1 = /at/g
// 匹配第一个bat或cat,忽略大小写
const pattern2 = /[bc]at/i
// 匹配所有以at结尾的三字符组合,忽略大小写
const pattern3 = /.at/gi
与其他语言中的正则表达式类似,所有元字符在模式中也必须转义,包括:
( [ { \ ^ $ | ) ] } ? * + .
元字符
在正则表达式中都有一种或多种特殊功能,所以要匹配上面这些字符本身,就必须使用反斜杠来转义。下面是几个例子:
// 匹配第一个bat或cat,忽略大小写
const pattern1 = /[bc]at/i
// 匹配第一个[bc]at,忽略大小写
const pattern2 = /\[bc\]at/i
// 匹配所有以at结尾的三字符组合,忽略大小写
const pattern3 = /.at/gi
// 匹配所有.at,忽略大小写
const pattern4 = /\.at/gi
这里的pattern1
匹配bat
或cat
,不区分大小写。要直接匹配[bc]at
,左右中括号都必须像pattern2
中那样使用反斜杠转义。在pattern3
中,点号表示at
前面的任意字符都可以匹配。如果想匹配.at
,那么要像pattern4
中那样对点号进行转义。
前面例子中的正则表达式都是使用字面量形式定义的。正则表达式也可以使用RegExp构造函数
来创建,它接收两个参数:模式字符串和(可选的)标记字符串。任何使用字面量定义的正则表达式也可以通过构造函数来创建,比如:
// 匹配第一个bat或cat,忽略大小写
const pattern1 = /[bc]at/i
// 跟pattern1一样,只不过是用构造函数创建的
const pattern2 = new RegExp('[bc]at', 'i')
这里的pattern1
和pattern2
是等效的正则表达式。
RegExp构造函数的两个参数都是字符串。
因为RegExp
的模式参数是字符串,所以在某些情况下需要二次转义。所有元字符都必须二次转义,包括转义字符序列,如\n
(\
转义后的字符串是\\
,在正则表达式字符串中则要写成\\\\
)。下表展示了几个正则表达式的字面量形式,以及使用RegExp
构造函数创建时对应的模式字符串。
字面量模式 | 对应的字符串 |
---|---|
/\[bc\]at/ | "\\[bc\\]at" |
/\.at/ | "\\.at" |
/name\/age/ | "name\\/age" |
/\d.\d{1,2}/ | "\\d.\\d{1,2}" |
/\w\\hello\\123/ | "\\w\\\\hello\\\\123" |
使用RegExp
也可以基于已有的正则表达式实例,并可选择性地修改它们的标记:
const re1 = /cat/g
console.log(re1) // "/cat/g"
const re2 = new RegExp(re1)
console.log(re2) // "/cat/g"
const re3 = new RegExp(re1, 'i')
console.log(re3) // "/cat/i"
二、RegExp 实例属性
每个RegExp
实例都有下列属性,提供有关模式的各方面信息。
global
:布尔值,表示是否设置了g
标记。ignoreCase
:布尔值,表示是否设置了i
标记。unicode
:布尔值,表示是否设置了u
标记。sticky
:布尔值,表示是否设置了y
标记。lastIndex
:整数,表示在源字符串中下一次搜索的开始位置,始终从0
开始。multiline
:布尔值,表示是否设置了m
标记。dotAll
:布尔值,表示是否设置了s
标记。source
:正则表达式的字面量字符串(不是传给构造函数的模式字符串),没有开头和结尾的斜杠。flags
:正则表达式的标记字符串。
通过这些属性可以全面了解正则表达式的信息,不过实际开发中用得并不多,因为模式声明中包含这些信息。下面是一个例子:
const pattern1 = /\[bc\]at/i
console.log(pattern1.global) // false
console.log(pattern1.ignoreCase) // true
console.log(pattern1.multiline) // false
console.log(pattern1.lastIndex) // 0
console.log(pattern1.source) // \[bc\]at
console.log(pattern1.flags) // i
const pattern2 = new RegExp('\\[bc\\]at', 'i')
console.log(pattern2.global) // false
console.log(pattern2.ignoreCase) // true
console.log(pattern2.multiline) // false
console.log(pattern2.lastIndex) // 0
console.log(pattern2.source) // \[bc\]at
console.log(pattern2.flags) // i
虽然第一个模式是通过字面量创建的,第二个模式是通过RegExp
构造函数创建的,但两个模式的source
和flags
属性是相同的。source
和flags
属性返回的是规范化之后可以在字面量中使用的形式。
三、选择(OR)
选择是正则表达式中的一个术语,实际上是一个简单的或
。
在正则表达式中,它用竖线 |
表示。
例如,我们需要找出编程语言:html
、css
。
对应的正则表达式为:html|css
。
const reg = /html|css/gi
let str = 'First HTML appeared, then CSS'
console.log(str.match(reg)) // ['HTML', 'CSS']
四、量词
用来形容我们所需要的数量的词被称为量词
。
-
{m,}
表示至少出现m次。 -
{m}
等价于{m,m}
,表示出现m
次。 -
?
等价于{0,1}
,表示出现或者不出现。 -
+
等价于{1,}
,表示出现至少一次。 -
*
等价于{0,}
,表示出现任意次,有可能不出现。
五、贪婪匹配和惰性匹配
正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。如下面的例子匹配出数字后面的0
:
const reg = /^(\d+)(0*)$/
const res = reg.exec('102300')
console.log(res) // ['102300', '102300', '', index: 0, input: '102300', groups: undefined]
由于\d+
采用贪婪匹配,直接把后面的0
全部匹配了,结果0*
只能匹配空字符串了。必须让\d+
采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0
匹配出来,加个?
就可以让\d+
采用非贪婪匹配:
const reg = /^(\d+?)(0*)$/
const res = reg.exec('102300')
console.log(res) // ['102300', '1023', '00', index: 0, input: '102300', groups: undefined]
六、字符组
[]
字符组表示在同一个位置可能出现的各种字符,也就是说它的匹配结果只能是一个字符,不能是多个。
6.1、字符范围
方括号也可以包含字符范围
。如果字符组里的字符特别多的话,可以使用字符范围。
比如[123456abcdefGHIJKLM]
,可以写成[1-6a-fG-M]
。用连字符-
来省略和简写。
因为连字符有特殊用途,要匹配a
、-
、z
这三者中任意一个字符。可以写成如下的方式:
[-az]
或[az-]
或[a-z]
。要么放在开头,要么放在结尾,要么转义。总之不会让引擎认为是范围表示法就行了。
6.2、排除范围
除了普通的范围匹配,还有类似 [^…]
的“排除”范围匹配。
它们通过在匹配查询的开头添加插入符号 ^
来表示,它会匹配所有除了给定的字符
之外的任意字符。
例如[^abc]
,表示是一个除a
、b
、c
之外的任意一个字符。字符组的第一位放^
(脱字符),表示求反的概念。
6.3、常见的简写形式
\d
是[0-9]
:表示是一位数字。
\D
是[^0-9]
:表示除数字外的任意字符。
\w
是[0-9a-zA-Z_]
:表示数字、大小写字母和下划线。w是word的简写,也称单词字符。
\W
是[^0-9a-zA-Z_]
:表示非数字、大小写字母和下划线。
\s
是[ \t\v\n\r\f]
:表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。
\S
是[^ \t\v\n\r\f]
:非空白符。
.
就是[^\n\r\u2028\u2029]
:通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。
七、位置匹配
正则表达式是匹配的模式之一:位置匹配。这点很重要。
位置是相邻字符之间的位置。比如,下图中箭头所指的地方:
在ES5
中,共有6个锚字符匹配位置:
^
$
\b
\B
(?=p)
(?!p)
7.1、字符串开始 ^ 和末尾 $
插入符号 ^
和美元符号 $
在正则表达式中具有特殊的意义。它们被称为锚点
。
插入符号 ^
匹配文本开头,而美元符号 $
则匹配文本末尾。
举个例子,让我们测试一下文本是否以 html
开头:
const result = /^html/.test('html js')
console.log(result) // true
该模式 ^html
的意思是:字符串开始,接着是html
。
与此类似,我们可以用 js$
来测试文本是否以 js
结尾:
const result = /js$/.test('html js')
console.log(result) // true
^...$
放在一起常常被用于测试一个字符串是否完全匹配一个模式。比如,测试用户的输入是否符合正确的格式。
让我们测试一下一个字符串是否属于 12:34
格式的时间。两个数字,然后一个冒号,接着是另外两个数字。
const success = '12:34'
const fail = '12:345'
const reg = /^\d\d:\d\d$/
console.log(reg.test(success)) // true
console.log(reg.test(fail)) // false
整个字符串必须准确地符合这一个格式。如果其中有任何偏差或者额外的字符,结果将为 false
。
7.2、m 多行模式
当修饰符 m
出现时,锚点将会有不同的行为。通过 flag /.../m
可以开启多行模式。它只会影响 ^
和 $
锚符的行为。
在多行模式下,它们不仅仅匹配文本的开始与结束,还匹配每一行的开始与结束。
7.2.1、行的开头
在这个有多行文本的例子中,正则表达式 /^\d+/gm
将匹配每一行的开头数字:
const str = '1st\n2nd\n33rd'
console.log(str.match(/^\d+/gm)) // ['1', '2', '33']
没有/.../m
时,仅仅是第一个数字被匹配到:
const str = '1st: html\n2nd: js\n3rd: css'
console.log(str.match(/^\d+/g)) // ['1']
这是因为默认情况下,锚符 ^
仅仅匹配文本的开头,在多行模式下,它匹配行的开头。
7.2.2、行的结尾
正则表达式 \w+$
会找到每一行的最后一个单词:
const str = '1st: html\n2nd: js\n3rd: css'
console.log(str.match(/\w+$/gm)) // ['html', 'js', 'css']
没有 /.../m
的话,美元符 $
将会仅仅匹配整个文本的结尾,所以只有最后的一个单词会被找到。
const str = '1st: html\n2nd: js\n3rd: css'
console.log(str.match(/\w+$/g)) // ['css']
7.3、\b 词边界
当正则表达式引擎遇到 \b
时,它会检查字符串中的位置是否是词边界。具体就是\w
和\W
之间的位置,也包括\w
和^
之间的位置,也包括\w
和$
之间的位置。
有三种不同的位置可作为词边界:
- 在字符串开头,如果第一个字符是单词字符
\w
。 - 在字符串中的两个字符之间,其中一个是单词字符
\w
,另一个不是。 - 在字符串末尾,如果最后一个字符是单词字符
\w
。
// @Hello@,@JavaScript@!
console.log('Hello,JavaScript!'.replace(/\b/g, '@'))
我们可以看@Hello@,@JavaScript@!
是怎么来的:
- 第一个
@
,位置是开头,第一个字符是H
是\w
。是^
和H
之间的位置。 - 第二个
@
,两边是o
与,
,也就是\w
和\W
之间的位置。 - 第三个
@
,两边是,
与"J",也就是\w
和\W
之间的位置。 - 第四个
@
,两边是t
与!
,也就是\w
和\W
之间的位置。
在字符串 Hello, JavaScript!
中,以下位置对应于 \b
:
7.4、\B
非词边界
\B
就是\b
的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉\b
,剩下的都是\B
的。
具体说来就是\w
与\w
、\W
与\W
、^
与\W
,\W
与$
之间的位置。
比如上面的例子Hello,JavaScript!
,把所有\B
替换成@
:
// H@e@l@l@o,J@a@v@a@S@c@r@i@p@t!@
console.log('Hello,JavaScript!'.replace(/\B/g, '@'))
7.5、零宽断言
在使用正则表达式时,有时我们需要捕获的内容前后必须是特定内容,但又不捕获这些特定内容的时候,零宽断言就起到作用了。
零宽断言正如它的名字一样,是一种零宽度的匹配,它匹配到的内容不会保存到匹配结果中去,最终匹配结果只是一个位置而已(不会占用index
宽度)。
零宽断言
分为4
类:
- 正向零宽先行断言
(?=exp)
:目标字符出现的位置的右边必须匹配到exp
这个表达式,也就是exp
前面的那个位置。 - 负向零宽先行断言
(?!exp)
:目标字符出现的位置的右边不能匹配到exp
这个表达式。 - 正向零宽后行断言
(?<=exp)
:目标字符出现的位置的左边必须匹配到exp
这个表达式。 - 负向零宽后行断言
(?<!exp)
:目标字符出现的位置的左边不能匹配到exp
这个表达式。
ES5
支持先行断言(即支持(?=exp)
和?!exp
),ES2018
才支持后行断言。
下面是正向零宽先行断言的例子:
const str = 'abCD123'
const reg = /ab(?=[A-Z])/
console.log(str.match(reg)) // ['ab', index: 0, input: 'abCD123', groups: undefined]
在以上代码中,正则表达式的语义是:匹配后面跟随任意一个大写字母的字符串ab
。最终匹配结果是ab
,因为零宽断言(?=[A-Z])
并不匹配任何字符,只是用来规定当前位置的后面必须是一个大写字母。
下面是负向零宽先行断言的例子:
const str = 'abCD123'
const reg = /ab(?![A-Z])/
console.log(str.match(reg)) // null
以上代码中,正则表达式的语义是:匹配后面不跟随任意一个大写字母的字符串ab
。正则表达式没能匹配任何字符,因为在字符串中,ab
的后面跟随有大写字母。
八、分组(捕获组)
在正则表达式中,使用()
进行分组,一对圆括号括起来的表达式就是一个分组,也叫捕获组。
使用分组会有两个作用:
- 将匹配的一部分作为结果数组中的单独项。
- 如果我们将量词(比如:
(html)+
)放在括号后,则它将括号视为一个整体。
下面看一个示例:不带括号,模式go+
表示g
字符,其后o
重复一次或多次。例如 goooo
或 gooooooooo
。括号可以将字符组合,所以(go)+
匹配 go
,gogo
,gogogo
等:
'Gogogo now!'.match(/(go)+/gi) // ['Gogogo']
还可以使用括号加选择的方式进行组合(如:(html|css)
)。比如,要匹配如下的字符串:
I Love html
I Love css
可以使用下面正则:
const reg = /^I Love (html|css)$/
reg.test('I Love html') // true
reg.test('I Love css') // true
8.1、数据提取
除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()
表示的就是要提取的分组(Group
)。
比如^(\d{3})-(\d{3,8})$
分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:
const reg = /^(\d{3})-(\d{3,8})$/
console.log(reg.exec('010-12345')) // ['010-12345', '010', '12345', index: 0, input: '010-12345', groups: undefined]
console.log(reg.exec('010 12345')) // null
8.2、嵌套组
括号可以嵌套。在这种情况下,编号从左到右。
例如,在搜索 html 123
时我们可能需要得到下面的内容:
- 整个内容:
html 123
。 - 英文:
html
。 - 数字:
123
。
让我们为它们添加括号:(([a-z]+)\s*(\d+))
。
这是它们的编号方式(从左到右,由左括号开始):
const str = 'html 123'
const reg = /(([a-z]+)\s*(\d+))/
const res = reg.exec(str)
// 匹配到的整个字符串
console.log(res[0]) // html 123
// 匹配成功的子串
console.log(res[1]) // html 123
// 匹配成功的子串
console.log(res[2]) // html
// 匹配成功的子串
console.log(res[3]) // 123
九、非捕获组
非捕获组的语法是在捕获组的基础上,在左括号的右侧加上?:
就可以了,那就是
(?:)。
(\d)
表示捕获组,而(?:\d)
表示非捕获组。既然是非捕获组,那它就不会把正则匹配到的内容保存到分组里面。
一旦使用了()
,就会默认为是普通捕获组,从而将()
内表达式匹配的内容捕获到组里。但是有些情况下,不得不用()
,但并不关心()
中匹配的内容是什么,后面也不会引用捕获到的内容,这带来了一个副作用,记录这些捕获组就会占用内存,降低匹配效率。
设计非捕获组的目的就是为了抵消这种副作用。 只进行分组,并不将子表达式匹配到的内容捕获到组里。
例如,如果我们要查找 (go)+
,但不希望括号内容(go
)作为一个单独的数组项,则可以编写:(?:go)+
。在下面的示例中,我们仅将名称 JS
作为匹配项的单独成员:
const str = 'Gogogo John!'
// ?: 从捕获组中排除 'go'
const regexp = /(?:go)+ (\w+)/i
const result = str.match(regexp)
console.log(result[0]) // Gogogo John(完全匹配)
console.log(result[1]) // John
console.log(result.length) // 2(数组中没有更多项)
9.1、什么时候该用非捕获组
-
不需要用到分组里面的内容的时候,用非捕获组,主要是为了提升效率,因为捕获组多了一步保存数据的步骤,所以一般会多耗费一些时间,虽然时间很短。
-
不考虑效率的场合,可以不用非捕获组,以提高正则表达式的可读性。
-
一些非常简单的正则中,如果使用了非捕获组,因为要解析这种语法,反而可能会降低匹配效率。
十、回溯
目前实现正则表达式引擎的方式有两种:DFA
自动机(Deterministic Final Automata 确定有限状态自动机)和NFA
自动机(Non deterministic Finite Automaton 非确定有限状态自动机)。
NFA
自动机的优势是支持更多功能。例如,捕获group
、环视、占有优先量词等高级功能。这些功能都是基于子表达式独立进行匹配,因此在编程语言里,使用的正则表达式库都是基于NFA
实现的。
上面这些概念作为了解即可。
NFA
自动机到底是怎么进行匹配的呢?
NFA
自动机会读取正则表达式的每一个字符,拿去和目标字符串匹配,匹配成功就换正则表达式的下一个字符,反之就继续和目标字符串的下一个字符进行匹配。
以text="aabcab" regex="bc"
字符和表达式来举例说明:
首先,读取正则表达式的第一个匹配符和字符串的第一个字符进行比较,b
对a
,不匹配。继续换字符串的下一个字符,也是a
,不匹配。继续换下一个,是b
,匹配。
然后同理,读取正则表达式的第二个匹配符和字符串的第四个字符进行比较,c
对c
,匹配。继续读取正则表达式的下一个字符,然而后面已经没有可匹配的字符了,结束。
这就是NFA
自动机的匹配过程,虽然在实际应用中,碰到的正则表达式都要比这复杂,但匹配方法是一样的。
10.1、NFA自动机的回溯
用NFA
自动机实现的比较复杂的正则表达式,在匹配过程中经常会引起回溯问题。大量的回溯会长时间地占用CPU
,从而带来系统性能开销。以下面代码举例说明:
text="abbc" regex="ab{1,3}c"
这个例子,匹配目的比较简单。匹配以a
开头,以c
结尾,中间有1-3
个b
字符的字符串。NFA
自动机对其解析的过程是这样的:
- 首先,读取正则表达式第一个匹配符
a
和字符串第一个字符a
进行比较,a
对a
,匹配。
- 然后,读取正则表达式第二个匹配符
b{1,3}
和字符串的第二个字符b
进行比较,匹配。但因为b{1,3}
表示1-3
个b
字符串,NFA
自动机又具有贪婪特性,所以此时不会继续读取正则表达式的下一个匹配符,而是依旧使用b{1,3}
和字符串的第三个字符b
进行比较,结果还是匹配。
- 接着继续使用
b{1,3}
和字符串的第四个字符c
进行比较,发现不匹配了,此时将要发生回溯。
- 发生回溯,已经读取的字符串第四个字符
c
将被吐出去,指针回到第三个字符b
的位置。
- 那么发生回溯以后,匹配过程怎么继续呢?程序会读取正则表达式的下一个匹配符
c
,和字符串中的第四个字符c
进行比较,结果匹配,结束。
十一、JS RegExp实例方法
11.1、exec()
如果正则表达式中定义了组,就可以在RegExp
对象上用exec()
方法提取出子串来。这个方法只接收一个参数,返回字符串str
中的regexp
匹配项。
exec()
方法在匹配成功后,会返回一个Array
,第一个元素是正则表达式匹配到的整个字符串,后面的字符串表示匹配成功的子串(在第一个元素匹配到的整个字符串的基础上)。如果没找到匹配项,则返回null
。
返回的数组虽然是Array
的实例,但包含三个额外的属性:index
和input
。index
是字符串中匹配模式的起始位置,input
是要查找的字符串。groups
是一个新的属性,用来存储命名捕获组的信息。
下面是提取出年、月、日的例子:
const reg = /(\d{4})-(\d{2})-(\d{2})/
const str = '2020-02-11'
const result = reg.exec(str)
console.log(result.index) // 0
console.log(result.input) // 2020-02-11
console.log(result[0]) // 2020-02-11
console.log(result[1]) // 2020
console.log(result[2]) // 02
console.log(result[3]) // 11
在上面这个例子中,模式包含三个捕获组:(\d{4})
、(\d{2})
、(\d{2})
,因为整个字符串匹配,所以matchs
数组的index
属性就是0
。数组的第一个元素是匹配的整个字符串,第二个元素是匹配第一个捕获组的字符串,第三个元素是匹配第二个捕获组的字符串。第四个元素是匹配第三个捕获组的字符串。
11.1.1、无全局标记 g
如果没有设置全局标记g
,则无论对同一个字符串调用多少次exec()
,也只会返回第一个匹配的信息。
const str = 'cat, bat, sat'
const reg = /.at/
let res = reg.exec(str)
console.log(res[0]) // cat
console.log(res.index) // 0
console.log(reg.lastIndex) // 0
res = reg.exec(str)
console.log(res[0]) // cat
console.log(res.index) // 0
console.log(reg.lastIndex) // 0
上面例子中的模式没有设置全局标记,因此调用exec()
只返回第一个匹配项cat
。lastIndex
在非全局模式下始终不变。
11.1.2、有全局标记 g
如果模式设置了全局标记g
,则每次调用exec()
方法会返回一个匹配的信息。每次调用exec()
都会在字符串中向前搜索下一个匹配项,如下面的例子所示:
const str = 'cat, bat, sat'
let reg = /.at/g
let res = reg.exec(str)
console.log(res[0]) // cat
console.log(res.index) // 0
console.log(reg.lastIndex) // 3
res = reg.exec(str)
console.log(res[0]) // bat
console.log(res.index) // 5
console.log(reg.lastIndex) // 8
res = reg.exec(str)
console.log(res[0]) // sat
console.log(res.index) // 10
console.log(reg.lastIndex) // 13
res = reg.exec(str)
console.log(res) // null
这次模式设置了全局标记g
,那么会按照下面方式执行:
-
调用
regexp.exec(str)
会返回第一个匹配项,并将紧随其后的位置保存在属性regexp.lastIndex
中。 -
下一次同样的调用会从位置
regexp.lastIndex
开始搜索,返回下一个匹配项,并将其后的位置保存在regexp.lastIndex
中。 -
…以此类推。
-
如果没有匹配项,则
regexp.exec
返回null
,并将regexp.lastIndex
重置为0
。
我们可以通过手动设置lastIndex
,用regexp.exec
从指定位置进行搜索。例如:
const str = 'Hello, world!'
const regexp = /\w+/g
// 从第 5 个位置搜索(从逗号开始)
regexp.lastIndex = 5
console.log(regexp.exec(str)[0]) // world
11.1.3、粘附标记 y
如果模式设置了粘附标记y
,则每次调用exec()
就只会精确的在lastIndex
的位置上寻找匹配项。粘附标记覆盖全局标记。
const str = 'cat, bat, sat, fat'
let reg = /.at/gy
let res = reg.exec(str)
console.log(res[0]) // cat
console.log(res.index) // 0
console.log(reg.lastIndex) // 3
res = reg.exec(str)
// 以索引3对应的字符开头找不到匹配项,因此exec()返回null
console.log(res) // null
// exec()没找到匹配项,于是将 lastIndex 设置为 0
console.log(reg.lastIndex) // 0
// 设置 lastIndex 可以让粘附的模式通过 exec()找到下一个匹配项
reg.lastIndex = 5
res = reg.exec(str)
console.log(res[0]) // bat
console.log(res.index) // 5
console.log(reg.lastIndex) // 8
11.2、test()
正则表达式的另一个方法是test()
,它接收一个字符串参数。如果输入的文本与模式匹配,则参数返回true
,否则返回false
。这个方法适用于只想测试模式是否匹配,而不需要实际匹配内容的情况。test()
经常用在if
语句中:
const str = '000-00-0000'
const reg = /\d{3}-\d{2}-\d{4}/
if (reg.test(str)) {
console.log('success') // success
}
在这个例子中,如果输入的文本与模式匹配,则显示匹配成功的消息。这个用法常用于验证用户输入。
11.3、toLocaleString()、toString()和valueOf()
无论正则表达式是怎么创建的,继承的方法toLocaleString()
和toString()
都返回正则表达式的字面量表示。比如:
const reg = new RegExp('\\[bc\\]at', 'gi')
console.log(reg.toString()) // /\[bc\]at/gi
console.log(reg.toLocaleString()) // /\[bc\]at/gi
这里的模式是通过RegExp
构造函数创建的,但toLocaleString()
和toString()
返回的都是其字面量的形式。
正则表达式的valueOf()
方法返回正则表达式本身:
const reg = new RegExp('\\[bc\\]at', 'gi')
console.log(reg.valueOf()) // /\[bc\]at/gi
console.log(Object.prototype.toString.call(reg.valueOf())) // [object RegExp]
十二、JavaScript字符串(String)的方法
本章节将探讨与字符串和正则表达式配合使用的各种方法。
12.1、replace()
replace()
这个方法接收两个参数:
-
第一个参数:可以是一个字符串或者是一个正则表达式。
-
第二个参数:是一个字符串也可以是一个函数。
注意:原字符串不会改变。
StringObject.replace(searchValue,replaceValue):
-
StringObject:字符串
-
searchValue:字符串或正则表达式
-
replaceValue:字符串或者函数
-
返回值:一个部分或全部匹配新的字符串。
上述介绍后,replace()方法就有如下的4种使用方式:
- 第一个参数为
字符串
,第二个参数也是字符串
- 第一个参数是
字符串
,第二个参数是函数
- 第一个参数是
正则表达式
,第二参数是字符串
- 第一个参数是
正则表达式
,第二个参数是函数
下面会逐一介绍这四种使用方式。
12.1.1、第一个参数传入字符串,第二个参数传入字符串
如果第一个参数是字符串,第二个参数也是字符串,只会替换第一个子字符串。要想替换所有子字符串,唯一的办法就是提供一个正则表达式,并且要指定全局g
标志。
const text = 'cat,bat,sat,fat';
const result = text.replace('at','ond');
console.log(result) // cond,bat,sat,fat
12.1.2、第一个参数传入字符串,第二个参数传入函数
第一个参数是字符串,第二个参数是函数的方式的情况下,在只有一个匹配项时(第一个参数是字符串所以只有一个匹配项)会向这个函数传递三个参数,函数的返回值将替换字符串的匹配部分:
- 模式的匹配项。
- 模式的匹配项在字符串中的位置。
- 原始字符串。
const text = 'cat,bat,sat,fat'
const result = text.replace('at', function (match, pos, orginalText) {
console.log(match) // at
console.log(pos) // 1
console.log(orginalText) // cat,bat,sat,fat
return 'ond' // 返回值为需要替换的新值
})
console.log(result) // cond,bat,sat,fat
12.1.3、第一个参数传入正则表达式,第二个参数传入字符串
如果第一个参数是正则表达式并带有g
,就会替换所有子字符串。
const text = 'cat,bat,sat,fat'
const result = text.replace(/at/g, 'ond')
console.log(result) // cond,bond,sond,fond
如果第二个参数是字符串的话,还可以用特殊的字符序列,将正则表达式匹配得到的值插入结果字符串中。
字符序列 | 替换文本 |
---|---|
$& | 匹配整个模式的子字符串。与RegExp.lastMatch 相同 |
$ | 匹配子字符串之前的字符串 (匹配结果前面的文本) |
$' | 匹配子字符串之后的字符串 (匹配结果后面的文本) |
$n | 匹配第n 个捕获组的子字符串(n = 0~9 )。如:$1 匹配第一个捕获的字符串,$2 是匹配第二个捕获的字符串,以此类推。如果没有捕获组,则值为空字符串。 |
$nn | 匹配第nn 个捕获组的字符串,(n = 01~99 )。如:$01 匹配第一个捕获的字符串,$02 是匹配第二个捕获的字符串,以此类推。如果没有捕获组,则值为空字符串。 |
- 使用
$&
字符可以重复引用匹配的文本:
// $& 表示匹配的字符串,即`b`本身
const text = 'abab'
const result = text.replace('b', '{$&}')
console.log(result) //a{b}ab
- 使用
$'
和$`字符替换内容:
const text = 'abbc'
// $`表示匹配结果之前的字符串
// 第一个`b`,$` 指代`a`
const result1 = text.replace('b', '{$`}')
console.log(result1) // a{a}bc
// $'表示匹配结果之后的字符串
// 第一个`b`,$' 指代`bc`
const result2 = text.replace('b', "{$'}")
console.log(result2) // a{bc}bc
- 使用分组匹配组合新的字符串(
$n
$nn
):交换一个字符串中两个单词的位置,这个脚本使用$1
和$2
代替替换文本。
const reg = /(ab)(bc)/g
const text = 'abbc'
// $1 表示正则表达式的第一个匹配组,指ab
// $2 表示正则表达式的第二个匹配组,指bc
const result = 'abbc'.replace(/(ab)(bc)/, '$2$1')
console.log(result) // bcab
12.1.4、第一个参数传入正则表达式和第二个参数传入函数
第一个参数传入正则表达式,第二个参数传入函数的方式的时候,在每次执行只有一个匹配项时会向这个函数传递三个参数,函数的返回值将替换字符串的匹配部分:
- 模式的匹配项。
- 模式的匹配项在字符串中的开始位置。
- 原始字符串。
下面是一个过滤特殊字符的例子:
function htmlEscape(text) {
return text.replace(/[<>"&]/g, function (match, pos, originalText) {
// ['<', 0, '<div class="root">Hello world!</div>']
// ['"', 11, '<div class="root">Hello world!</div>']
// ['"', 16, '<div class="root">Hello world!</div>']
// ['>', 17, '<div class="root">Hello world!</div>']
// ['<', 30, '<div class="root">Hello world!</div>']
// ['>', 35, '<div class="root">Hello world!</div>']
console.log(Array.from(arguments)) // 一共有6个匹配项,函数会执行6次,所以会依次输出6次
switch (match) {
case '<':
return '<'
case '>':
return '>'
case '&':
return '&'
case '"':
return '"'
}
})
}
const result = htmlEscape(`<div class="root">Hello world!</div>`)
console.log(result) // <div class="root">Hello world!</div>
函数htmlEscape()
用于将一段HTML
中的4
个字符替换成对应的实体:小于号、大于号、
和号,还有双引号,都必须经过转义。这个函数返回一个字符串,表示应该把匹配项替换成什么。根据匹配的每个字符分别返回特定的HTML
实体。
在正则表达式定义了多个捕获组的情况下,传递给函数的参数依次是func(match, p1, p2, ..., pn, offset, input, groups)
:
match
:模式的匹配项。p1, p2, ..., pn
:第一个捕获组的匹配项、第二个捕获组的匹配项……。offset
:与整个模式匹配的开始位置。input
:原始字符串。
在下面的示例中,有两对括号,因此将使用5
个参数调用替换函数:第一个是完全匹配项,然后是2
对括号,然后是匹配位置和源字符串:
const str = 'html@css'
const result = str.replace(/(\w+)@(\w+)/, function (match, p1, p2, offset, input, groups) {
console.log(Array.from(arguments)) // ['html@css', 'html', 'css', 0, 'html@css']
return p1 // html
})
console.log(result) // html
如果有许多组,用rest
参数(…
)可以很方便的访问:
const str = 'html@css'
const result = str.replace(/(\w+)@(\w+)/, function (...args) {
console.log(args) // ['html@css', 'html', 'css', 0, 'html@css']
return args[2] // css
})
console.log(result) // css
12.2、replaceAll()
ES2021
引入了replaceAll()
方法,可以一次性替换所有匹配。它的用法与replace()
相同,返回一个新字符串,不会改变原字符串。
replaceAll()
这个方法接收两个参数:
-
第一个参数:可以是一个字符串或者是一个正则表达式。
-
第二个参数:是一个字符串也可以是一个函数。
StringObject.replaceAll(searchValue,replaceValue):
-
StringObject:字符串
-
searchValue:字符串或正则表达式
-
replaceValue:字符串或者函数
-
返回值:一个部分或全部匹配新的字符串。
12.2.1、第一个参数传入字符串,第二个参数传入字符串
如果第一个参数是字符串,第二个参数也是字符串,会一次性替换所有匹配。
const text = 'cat,bat,sat,fat'
const result = text.replaceAll('at', 'ond')
console.log(result) // cond,bond,sond,fond
12.2.2、第一个参数传入字符串,第二个参数传入函数
第一个参数是字符串,第二个参数是函数的方式的情况下,会向这个函数传递三个参数,函数的返回值将替换字符串的匹配部分:
- 模式的匹配项。
- 模式的匹配项在字符串中的位置。
- 原始字符串。
const text = 'cat,bat,sat,fat'
const result = text.replaceAll('at', function (match, pos, orginalText) {
// ['at', 1, 'cat,bat,sat,fat']
// ['at', 5, 'cat,bat,sat,fat']
// ['at', 9, 'cat,bat,sat,fat']
// ['at', 13, 'cat,bat,sat,fat']
console.log(Array.from(arguments)) // 一共有4个匹配项,函数会执行4次,所以会依次输出4次
return 'ond' // 返回值为需要替换的新值
})
console.log(result) // cond,bond,sond,fond
12.1.3、第一个参数传入正则表达式,第二个参数传入字符串
如果第一个参数是正则表达式的时候,replaceAll
必须设置全局g
标识,否则replaceAll()
会报错。
const text = 'cat,bat,sat,fat'
// 报错
const result = text.replaceAll(/at/, 'ond') // Uncaught TypeError:
设置全局g
标识就不会有问题:
const text = 'cat,bat,sat,fat'
const result = text.replaceAll(/at/g, 'ond')
console.log(result) // cond,bond,sond,fond
如果第二个参数是字符串的话,还可以用特殊的字符序列,将正则表达式匹配得到的值插入结果字符串中(和replace()
相似)。
字符序列 | 替换文本 |
---|---|
$& | 匹配整个模式的子字符串。与RegExp.lastMatch 相同 |
$ | 匹配子字符串之前的字符串 (匹配结果前面的文本) |
$' | 匹配子字符串之后的字符串 (匹配结果后面的文本) |
$n | 匹配第n 个捕获组的子字符串(n = 0~9 )。如:$1 匹配第一个捕获的字符串,$2 是匹配第二个捕获的字符串,以此类推。如果没有捕获组,则值为空字符串。 |
$nn | 匹配第nn 个捕获组的字符串,(n = 01~99 )。如:$01 匹配第一个捕获的字符串,$02 是匹配第二个捕获的字符串,以此类推。如果没有捕获组,则值为空字符串。 |
- 使用
$&
字符可以重复引用匹配的文本:
// $& 表示匹配的字符串,即`b`本身
const text = 'abab'
const result = text.replaceAll('b', '{$&}')
console.log(result) //a{b}a{b}
- 使用
$'
和$`字符替换内容:
const text = 'abbc'
// $`表示匹配结果之前的字符串
// 第一个`b`,$` 指代`a`
// 第二个`b`,$` 指代`ab`
const result1 = text.replaceAll('b', '{$`}')
console.log(result1) // a{a}{ab}c
// $'表示匹配结果之后的字符串
// 第一个`b`,$' 指代`bc`
// 第二个`b`,$' 指代`c`
const result2 = text.replaceAll('b', "{$'}")
console.log(result2) // a{bc}{c}c
- 使用分组匹配组合新的字符串(
$n
$nn
):交换一个字符串中两个单词的位置。使用$1
和$2
代替替换文本。
const reg = /(ab)(bc)/g
const text = 'abbc'
// $1 表示正则表达式的第一个匹配组,指ab
// $2 表示正则表达式的第二个匹配组,指bc
const result = 'abbc'.replaceAll(/(ab)(bc)/g, '$2$1')
console.log(result) // bcab
12.1.4、第一个参数传入正则表达式和第二个参数传入函数
第一个参数传入正则表达式,第二个参数传入函数的方式的时候,在每次执行只有一个匹配项时会向这个函数传递三个参数,函数的返回值将替换字符串的匹配部分:
- 模式的匹配项。
- 模式的匹配项在字符串中的开始位置。
- 原始字符串。
下面是一个过滤特殊字符的例子:
function htmlEscape(text) {
return text.replace(/[<>"&]/g, function (match, pos, originalText) {
// ['<', 0, '<div class="root">Hello world!</div>']
// ['"', 11, '<div class="root">Hello world!</div>']
// ['"', 16, '<div class="root">Hello world!</div>']
// ['>', 17, '<div class="root">Hello world!</div>']
// ['<', 30, '<div class="root">Hello world!</div>']
// ['>', 35, '<div class="root">Hello world!</div>']
console.log(Array.from(arguments)) // 一共有6个匹配项,函数会执行6次,所以会依次输出6次
switch (match) {
case '<':
return '<'
case '>':
return '>'
case '&':
return '&'
case '"':
return '"'
}
})
}
const result = htmlEscape(`<div class="root">Hello world!</div>`)
console.log(result) // <div class="root">Hello world!</div>
函数htmlEscape()
用于将一段HTML
中的4
个字符替换成对应的实体:小于号、大于号、
和号,还有双引号,都必须经过转义。这个函数返回一个字符串,表示应该把匹配项替换成什么。根据匹配的每个字符分别返回特定的HTML
实体。
在正则表达式定义了多个捕获组的情况下,传递给函数的参数依次是func(match, p1, p2, ..., pn, offset, input, groups)
:
match
:模式的匹配项。p1, p2, ..., pn
:第一个捕获组的匹配项、第二个捕获组的匹配项……。offset
:与整个模式匹配的开始位置。input
:原始字符串。
在下面的示例中,有两对括号,因此将使用5
个参数调用替换函数:第一个是完全匹配项,然后是2
对括号,然后是匹配位置和源字符串:
const str = 'html@css'
const result = str.replaceAll(/(\w+)@(\w+)/g, function (match, p1, p2, offset, input, groups) {
console.log(Array.from(arguments)) // ['html@css', 'html', 'css', 0, 'html@css']
return p1 // html
})
console.log(result) // html
如果有许多组,用rest
参数(…
)可以很方便的访问:
const str = 'html@css'
const result = str.replaceAll(/(\w+)@(\w+)/g, function (...args) {
console.log(args) // ['html@css', 'html', 'css', 0, 'html@css']
return args[2] // css
})
console.log(result) // css
12.3、search()
search()
是查找模式的字符串方法。这个方法传入一个参数:正则表达式或RegExp
对象。
这个方法返回第一个匹配的位置索引,如果没找到则返回-1。始终从字符串开头向后匹配模式。
const text = 'cat, bat, sat, fat'
const pos = text.search(/at/)
console.log(pos) // 1
search(/at/)
返回1
,即"t
的第一个字符在字符串中的位置。
search
仅查找第一个匹配项。
12.4、match()
match()
方法接收一个参数,可以是一个正则表达式,也可以是一个RegExp
对象。在字符串str
中找到匹配regexp
的字符。
match()
的使用方式和exec()的使用方式基本一致,有一点需要注意,是否带有标志g
,它们的输出结果就不一样。
12.4.1、无全局标记 g
如果没有设置全局标记g
,那么str.match(regexp)
返回的第一个匹配和regexp.exec(str)
完全相同。
const str = 'cat, bat, sat'
const reg = /.at/
let res = str.match(reg)
console.log(res[0]) // cat
console.log(res.index) // 0
console.log(reg.lastIndex) // 0
res = str.match(reg)
console.log(res[0]) // cat
console.log(res.index) // 0
console.log(reg.lastIndex) // 0
12.4.2、有全局标记 g
如果设置全局标记g
,则会返回所有匹配上的内容:
const str = 'cat, bat, sat, fat'
let reg = /.at/g
let res = str.match(reg)
console.log(res) // ['cat', 'bat', 'sat', 'fat']
res = str.match(reg)
console.log(res) // ['cat', 'bat', 'sat', 'fat']
res = str.match(reg)
console.log(res) // ['cat', 'bat', 'sat', 'fat']
如果没有匹配项,则无论是否带有全局标记
g
,都将返回null
。
12.5、matchAll()
ES2020
增加了String.prototype.matchAll()
方法,可以一次性取出所有匹配。它返回的是一个可迭代对象,而不是数组。可以用for...of
循环取出。
const str = 'cat, bat, sat, fat'
const reg = /.at/g
const res = str.matchAll(reg)
console.log(res) // RegExpStringIterator {}
for (const m of res) {
// ['cat', index: 0, input: 'cat, bat, sat, fat', groups: undefined]
// ['bat', index: 5, input: 'cat, bat, sat, fat', groups: undefined]
// ['sat', index: 10, input: 'cat, bat, sat, fat', groups: undefined]
// ['fat', index: 15, input: 'cat, bat, sat, fat', groups: undefined]
console.log(m)
}
如果没有结果,则返回的不是
null
,而是一个空的可迭代对象。
12.6、split()
使用正则表达式(或子字符串)作为分隔符来分割字符串。
我们可以用split
来分割字符串,如下所示:
console.log('12-34-56'.split('-')) // ['12', '34', '56']
我们也可以用正则表达式来做:
console.log('12, 34, 56'.split(/,\s*/)) // ['12', '34', '56']
十三、使用小案例
使用自定义函数将字符串大写字母改为小写字母:
const result = 'JAVASCRIPT'.replace(/[A-G]/g,function(match,pos,orginalText){
return arguments[0].toLowerCase();
})
//JaVaScRIPT
console.log(result)
简单的JavaScript渲染模板:
function substitute (str, obj) {
return str.replace(/\{([^{}]+)\}/g, function(match, key) {
var value = obj[key];
return value !== undefined ? `${value}` : '';
});
}
const obj = {
url: 'http://www.163.com',
title: '我是标题',
text: '我是文本'
};
const link = '<a href="{url}" title="{title}">{text}</a>';
const result = substitute(link, obj);
//<a href="http://www.163.com" title="我是标题">我是文本</a>
console.log(result)
数字转为千分位:
const text = '1234567890';
const result = text.replace(/(\d)(?=(\d{3})+(?!\d))/g,'$1,');
//1,234,567,890
console.log(result)
驼峰和-
互相转换:
const text = 'myName'
const result = text.replace(/([A-Z])/g,'-$1').toLowerCase()
// my-name
console.log(result)
const text = 'my-name'
const result = text.replace(/-(\w)/g,function(match,matches,pos){
return matches.toUpperCase()
})
// myName
console.log(result)
把手机号第4位到第7位替换成****:
const text = '15560088888'
const result = text.replace(/1(\d{2})\d{4}(\d{4})/g,'1$1****$2')
// 155****8888
console.log(result)
十四、RegExp 构造函数属性
RegExp
构造函数的所有属性都没有任何Web
标准出处,因此不要在生产环境中使用它们。这里只做介绍。
RegExp
构造函数本身也有几个属性。这些属性适用于作用域中的所有正则表达式,而且会根据最后执行的正则表达式操作而变化。这些属性还有一个特点,就是可以通过两种不同的方式访问它们。换句话说,每个属性都有一个全名和一个简写。下表列出了RegExp
构造函数的属性。
全名 | 简写 | 说明 |
---|---|---|
input | $_ | 最后搜索的字符串(非标准特性) |
lastMatch | $& | 最近一次匹配的文本 |
lastParen | $+ | 最近一次捕获的文本(非标准特性) |
leftContext | $` | 字符串中出现在 lastMatch 前面的文本 |
rightContext | $' | 字符串中出现在 lastMatch 后面的文本 |
通过这些属性可以提取出与exec()
和test()
执行的操作相关的信息。来看下面的例子:
let str = 'this has been a short summer'
let reg = /(.)hort/g
if (reg.test(str)) {
console.log(RegExp.input) // this has been a short summer
// 注意有空格
console.log(RegExp.leftContext) // "this has been a "
console.log(RegExp.rightContext) // " summer"
console.log(RegExp.lastMatch) // short
console.log(RegExp.lastParen) // s
}
以上代码创建了一个模式,用于搜索任何后跟hort
的字符,并把第一个字符放在了捕获组中。不同属性包含的内容如下:
input
属性中包含原始的字符串。leftConext
属性包含原始字符串中short
之前的内容,rightContext
属性包含short
之后的内容。lastMatch
属性包含匹配最近一次匹配的文本,即short
。lastParen
属性包含最近一次捕获组的文本,即s
。
这些属性名也可以替换成简写形式,只不过要使用中括号语法来访问,如下面的例子所示,因为大多数简写形式都不是合法的ECMAScript
标识符:
let str = 'this has been a short summer'
let reg = /(.)hort/g
if (reg.test(str)) {
/*
* 注意:Opera 不支持简写属性名
* IE 不支持多行匹配
*/
console.log(RegExp.$_) // this has been a short summer
// 注意有空格
console.log(RegExp['$`']) // "this has been a "
console.log(RegExp["$'"]) // " summer"
console.log(RegExp['$&']) // short
console.log(RegExp['$+']) // s
}
RegExp
还有其他几个构造函数属性,可以存储最多9
个捕获组的匹配项。这些属性通过 RegExp.$1-RegExp.$9
来访问,分别包含第1~9
个捕获组的匹配项。在调用exec()
或 test()
时,这些属性就会被填充,然后就可以像下面这样使用它们:
let str = 'this has been a short summer'
let reg = /(..)mm(..)/g
if (reg.test(str)) {
console.log(RegExp.$1) // su
console.log(RegExp.$2) // er
}
在这个例子中,模式包含两个捕获组。调用test()
搜索字符串之后,因为找到了匹配项所以返回true
,而且可以打印出通过RegExp
构造函数的$1
和$2
属性取得的两个捕获组匹配的内容。
十五、参考
zh.javascript.info/regular-exp…