6.7 RegExp 类型
正则表达式(Regular Expression)是一种用来匹配字符串的工具,它可以根据用户指定的规则来进行字符串的匹配和搜索。正则表达式由一系列字符和特殊字符组成,用来定义一个搜索模式。通过正则表达式,可以快速地判断一个字符串是否符合特定的格式要求,或者从一个字符串中提取出需要的信息。正则表达式的应用非常广泛,比如在文本编辑器中进行查找替换、在程序中进行数据校验、在搜索引擎中进行模糊搜索等等。在 JavaScript 中,正则表达式对应的类型就是内置的 RegExp 类型,让开发者可以方便地进行字符串匹配和替换。
创建 RegExp 对象
在 JavaScript 中,可以使用字面量或构造函数的方式创建正则表达式对象。
使用字面量的方式:
const regex = /pattern/flags;
其中,pattern 是正则表达式的模式,flags 是一个可选参数,用于指定正则表达式的修饰符或者标识符。常用的修饰符有:
- 1 g:全局搜索;
- 2 i:忽略大小写;
- 3 m:多行搜索;
- 4 s:. 匹配任何字符,包括换行符;
- 5 u:使用 Unicode 码点进行匹配;
- 6 y:粘性搜索。
例如,创建一个匹配字符串 “hello” 的正则表达式对象:
const regex = /hello/;
使用构造函数的方式:
const regex = new RegExp(pattern, flags);
其中,pattern 和 flags 参数同上述方式。例如,创建一个与上述字面量相同的正则表达式对象:
const regex = new RegExp("hello");
一般情况下我们都采用字面量的方式创建,除非有一些正则表达式中有变量的情况,会使用 RegExp 参数是含有变量的字符串。
正则表达式语法
正则表达式由一系列字符和特殊字符组成,用来定义一个搜索模式。下面我们就来看看正则表达式中的语法。
修饰符
首先来看一下上面讲到的几种修饰符的作用。
- g:代表全局匹配,即匹配所有符合条件的内容,如果不指定这个修饰符,则正则表达式只会匹配第一个符合条件的内容。例如:
const regexg = /foo/g;
const regex = /foo/;
const str = 'foo foo';
console.log(str.match(regexg)); // ['foo', 'foo']
console.log(str.match(regex)); // ['foo']
后边会详细讲到字符串的 match 方法,这里第一个 match 的结果是 2 个 foo 都返回了,第二个 match 因为正则表达式没有 g 修饰符,则只会匹配第一个 foo。
- i:忽略大小写匹配,即不区分大小写进行匹配,默认是区分大小写,例如:
const regexi = /foo/i;
const regex = /foo/;
const str = 'FOO';
console.log(str.match(regexi)); // ["FOO"]
console.log(str.match(regex)); // null 没有匹配到
- m:多行匹配,即匹配每行开头和结尾处符合条件的内容。例如:
const regex = /^foo/gm;
const regexdefault = /^foo/g;
const str = 'foo\nfoo';
console.log(str.match(regex)); // ["foo", "foo"]
console.log(str.match(regexdefault)); // ["foo"]
在这里例子中,我们需要知道\n 在字符串中的是换行的意思,字符^在正则表达式中的作用是只匹配开始位置。所以在例子中,如果我们没有设置多行匹配,则只匹配了第一个 foo,而没有匹配后边的 foo,因为它不是开始位置,而这是了多行匹配之后,则匹配了每一行的开始位置。
- s:默认特殊字符点(.)是匹配除换行符(\n、\r)之外的任何单个字符,但是如果设置了 s 修饰符,则特殊字符点(.)匹配所有字符,包括换行符。例如:
const regex = /foo.bar/s;
const regexdefault = /foo.bar/;
const str = 'foo\nbar';
console.log(str.match(regexdefault)); // null
console.log(str.match(regex)); // ["foo\nbar"]
- u:使用 Unicode 码点进行匹配, 例如:
const regex = /\u{1F60A}/u;
const regexdefault = /\u{1F60A}/;
const str = '?';
console.log(str.match(regexdefault)); // null
console.log(str.match(regex)); // ["?"]
其中 \u{1F60A} 表示该表情的 Unicode 码点,u 修饰符则表示开启 Unicode 模式。
- y:粘性搜索,每次从上一次匹配成功的位置开始匹配。例如:
const regex = /foo/y;
const regexdefault = /foo/;
let str = 'foofoo';
console.log(str.match(regex)); // ['foo', index: 0]
console.log(str.match(regex)); // ['foo', index: 3]
console.log(str.match(regexdefault)); // ['foo', index: 0]
console.log(str.match(regexdefault)); // ['foo', index: 0]
match 方法实际上还会返回一个 index 属性,表示这次匹配的开始索引,从索引可以看出如果没有加 y 修饰符,则每次执行 match 方法匹配的都是第一个 foo,index 是 0。而设置了 y 修饰符的正则表达式,每次都会从上一次匹配成功的位置开始匹配,所以 index 分别是 0 和 3。注意这里没有设置全局搜索 g 修饰符,如果设置了则粘性搜索将失效,直接返回所有匹配。
字面量字符
正则表达式中的字面量字符是指直接匹配输入字符串中相应字符的字符,也叫做普通字符。例如 /abc/ 匹配的就是字符”abc”。普通字符包括了数字、大小写字母,下划线等等,除了在正则表达式中有特殊含义的其他字符,所以关键是掌握正则表达式中的特殊字符。下面就看看这些特殊的字符。
位置字符
在正则表达式中,位置字符用来描述字符串的位置信息,而不是匹配特定的字符。以下是常见的位置字符:
- 1 插入符号(^):用于匹配字符串的开头。例如,/^hello/:匹配以 hello 开头的字符串,对于输入字符串 “hello world”,该正则表达式可以匹配,而对于输入字符串 “world hello”,则不能匹配。
- 2 美元符号(/:匹配以 world 结尾的字符串,对于输入字符串 “hello world”,该正则表达式可以匹配,而对于输入字符串 “world hello”,则不能匹配。
- 3 单词边界(\b):用于匹配单词的边界,例如单词的开头或结尾,而不是单词中间的位置。例如,/\bhello\b/用于匹配单词 hello,hello 的左右都需要是单词的边界,结果如下:
/\bhello\b/.test('hello world'); // true
/\bhello\b/.test('helloworld'); // false
/\bhello/.test('helloworld'); // true 没有要求hello右边是单词边界
这里 test 方法是正则表达式对象的一个方法,用于检测是否匹配,后边会讲到,这里先知道它可以判断是否匹配就可以。
- 4 非单词边界(\B):与单词边界相反,匹配不在单词边界的位置。同样用上边的例子,把\b 换成\B,可以看下面的结果:
/\Bhello\B/.test('hello world'); // false hello左边是单词边界 不匹配
/\Bhello\B/.test('helloworld'); // false hello左边是单词边界 不匹配
/hello\B/.test('helloworld'); // true
需要注意的是,位置字符并不匹配具体的字符,它们只匹配特定的位置信息。例如,使用^匹配字符串的开头时,它只会匹配字符串的第一个字符,而不是整个字符串。位置字符通常用于限定匹配的范围,以确保匹配的内容符合特定的位置要求。
点字符 .
点字符 “.” 在正则表达式中默认匹配除了回车 “\r”、换行 “\n”、行分隔符 “\u2028” 和段分隔符 “\u2029” 之外的所有字符,包括 Unicode 字符。对于码点大于 0xFFFF 的 Unicode 字符,因为它们需要用两个 16 位的代码单元来表示,因此点字符无法正确匹配,会被认为是两个字符。
为了匹配任意字符(包括回车、换行、行分隔符、段分隔符和 Unicode 码点大于 0xFFFF 的字符),可以使用 “s” 修饰符,即在正则表达式的末尾加上 “s”。例如,正则表达式 “/./s” 可以匹配任意字符,包括回车、换行、行分隔符、段分隔符和 Unicode 码点大于 0xFFFF 的字符。需要注意的是,使用 “s” 修饰符可能会影响性能和安全性,应谨慎使用。
Unicode 码点大于 0xFFFF 的字符被称为扩展 Unicode 字符(Extended Unicode Characters),它们需要用两个 16 位的代码单元来表示。这些字符包括:
- 1 0x010000 到 0x10FFFF 范围内的所有字符,共 1,114,112 个字符。
- 2 包括一些不常用的汉字、藏文、藏文加点符等等,以及一些符号、表情等等。
需要注意的是,大部分常见的字符都落在 Unicode 码点小于等于 0xFFFF 的范围内,而扩展 Unicode 字符相对较少使用,通常只在一些特定的领域或文化中使用。在实际开发中,一般不需要考虑处理扩展 Unicode 字符的情况,除非是特定领域或文化的需求。
字符集合[]
在正则表达式中,字符集合用于表示一个字符的集合,该字符集合可以匹配其中的任何一个字符。字符集合由一对方括号([])括起来,方括号中列出的所有字符都是该字符集合的成员。例如,正则表达式 [abc] 匹配字符 a、b 或 c 中的任意一个,而不是匹配 abc 整体。
需要注意的是,字符集合中的字符顺序并不影响匹配结果。例如,[abc] 和 [cba] 都可以匹配 a、b 或 c 中的任意一个字符。在字符集合中,一些特殊字符(如.、|、*等)失去了特殊含义,只表示它们本身。例如,正则表达式 [a.b] 可以匹配字符 a、. 或 b 中的任意一个。
在字符集合内部,如果方括号的第一个字符是 “^”,表示取反操作,即匹配除了这些字符以外的任意一个字符。
例如,正则表达式 [^abc] 表示匹配除了 a、b、c 以外的任意一个字符。它可以匹配 “d”、”e”、”f” 等字符,但不能匹配 “a”、”b”、”c” 这三个字符。
需要注意的是,如果 “^” 不在字符集合内部的第一个位置,它就不再表示取反操作。比如,正则表达式 [a^bc] 表示匹配 “a”、”^”、”b” 或 “c” 中的任意一个字符。
连字符 –
在正则表达式中,连字符 “-” 通常用于字符集合中,用于表示一个字符范围。例如,正则表达式 [a-z] 表示匹配小写字母 a 到 z 中的任意一个字符。
连字符的使用要注意以下几点:
1 连字符必须放在字符集合中的两个字符之间,表示这两个字符范围内的所有字符都可以匹配。
2 如果连字符本身需要匹配,可以将其放在字符集合的开头或结尾位置,例如 [-a-z] 或 [a-z-]。
3 连字符只能用于表示 ASCII 字符集内的字符范围,不能用于 Unicode 字符集。
例如:
1 [a-z]:匹配小写字母 a 到 z 中的任意一个字符。
2 [A-Z]:匹配大写字母 A 到 Z 中的任意一个字符。
3 [0-9]:匹配数字 0 到 9 中的任意一个字符。
4 [a-zA-Z0-9]: 匹配 0-9 a-z A-Z 中的任意一个字符。
‘Hello123’.match(/[a-zA-Z0-9]/g)
// 输出 [‘H’, ‘e’, ‘l’, ‘l’, ‘o’, ‘1’, ‘2’, ‘3’]
选择符 |
在正则表达式中,选择符 | 用于表示“或”的关系,可以用来匹配多个模式中的任意一个。例如,正则表达式 “a|b” 表示匹配字符串中的 “a” 或 “b”。
选择符的使用要注意以下几点:
1 选择符通常用于多个子表达式之间,最好用使用括号将其括起来,例如 “(cat|dog)”,这样语意清晰。
2 如果多个子表达式之间没有使用括号,那么选择符的优先级最低,例如 cat|dog,如果不加括号,因为选择符的优先级最低,也就是最后计算它所以和(cat|dog)是相同的,假设选择符的优先级高的话 会是这样 ca(t|d)og。
3 如果选择符的两侧都是单个字符,可以使用字符集合来代替。例如,正则表达式 “a|b|c” 可以简写为 “[abc]”。
需要注意的是,在使用选择符时要特别注意正则表达式的语法和优先级,避免出现意外的匹配结果。
限定符
正则表达式中的限定符用于指定匹配的次数,常见的限定符包括:
- 1 *:匹配前面的表达式零次或多次。
- 2 +:匹配前面的表达式一次或多次。
- 3 ?:匹配前面的表达式零次或一次。
- 4 {n}:匹配前面的表达式恰好 n 次。
- 5 {n,}:匹配前面的表达式至少 n 次。
- 6 {n,m}:匹配前面的表达式至少 n 次,但不超过 m 次。
举个例子,如果我们要匹配连续的数字,可以使用 \d+。其中,\d 匹配一个数字,这个后边会再讲到,+ 表示匹配前面的表达式(\d)一次或多次。如果要匹配恰好 4 个数字,可以使用 \d{4};如果要匹配至少 4 个数字,可以使用 \d{4,}。
看如下例子:
// 匹配连续的数字,至少一个
let str1 = '123abc456def789xyz';
let reg1 = /\d+/g;
console.log(str1.match(reg1)); // ["123", "456", "789"]
// 匹配连续的数字,恰好四个
let str2 = '12ab345cd6789';
let reg2 = /\d{4}/g;
console.log(str2.match(reg2)); // ["6789"]
// 匹配手机号 1开头,第二位是3-9,后面9位是0-9
let phone = '13812345678';
let reg = /^1[3-9]\d{9}$/;
console.log(reg.test(phone)); // true
贪婪模式和非贪婪模式
因为有限定符的出现,字符匹配匹配的次数是不确定的,所以出现了贪婪模式和非贪婪模式的区别,在正则表达式中,匹配模式默认是贪婪模式。也就是说,尽可能多地匹配符合条件的字符。
例如,对于字符串 “abcde12345″,如果使用正则表达式 .\d 来匹配该字符串,结果会匹配整个字符串,因为 . 尽可能多地匹配了所有的字符,直到遇到最后一个数字 \d。
而非贪婪模式(Non-greedy mode)则是尽可能少地匹配符合条件的字符。
在正则表达式中,可以通过在限定符(、+、?、{n,m})后面添加 ? 来表示非贪婪模式。例如,使用正则表达式 .?\d 来匹配上面的字符串 “abcde12345″,结果只会匹配到 “abcde1″,因为 .*? 尽可能少地匹配符合条件的字符,直到遇到第一个数字 \d。
圆括号()
在正则表达式中,圆括号 “()” 用于表示正则表达式的分组,通常用于改变正则表达式中的运算符优先级或者对匹配结果进行捕获。具体来说,圆括号可以用于以下两种情况:
- 1 改变优先级:正则表达式中不同的运算符具有不同的优先级,如果需要改变优先级,可以使用圆括号进行分组,这个在讲选择符的以后有提到,例如 cat|dog,如果不加括号,因为选择符的优先级最低,也就是最后计算它所以和(cat|dog)是相同的,如果手动的添加上圆括号可以改变优先级,例如 ca(t|d)og, 则匹配的就是 ca 加 t 或者 d 加 og。
- 2 对分组应用限定符,有了分组就可以对多个字符或字符集合作为一个整体进行限定。例如 (ab)* 可以匹配 0 个或多个连续的 “ab” 子串,(a|b)+ 可以匹配 1 个或多个连续的 “a” 或 “b” 字符。
- 3 分组捕获:()内的内容算做一个组,之前讲到 match 方法返回的都是正则表达式匹配到的整体,有了分组之后,不仅可以返回整体,还可以把每个组匹配到的内容进行返回,例如:
'abcabc'.match(/(.)b(.)/); // 返回 ['abc', 'a', 'c']
注意不能使用全局匹配的修饰符 g,如果使用的话 就不会返回分组匹配的结果。
同时捕获的内容可以在正则表达式中通过\1、\2、\3 等形式进行引用。例如:
"this is a test middle test".match(/\b(\w+)\b.*\b\1\b/) // 返回 ['test middle test', 'test']
这个正则表达式\b 用于匹配单词边界,\w+用于匹配单词中的字符 同时把它作为一个捕获组,.*用于匹配任意字符(包括空格等),\1 用于引用第一个捕获组匹配到的内容。最后的结果是只有\w+匹配到 test 的时候是符合条件的。 - 4 非捕获分组:有些时候我们使用() 进行分组是为了对整体使用限定符,而不希望它进行捕获,这时候就可以使用 (?: ) 在括号内加上 ?: 将分组设置为非捕获分组,即匹配时不会将其作为一个独立的捕获组。例如,正则表达式 (?:ab)+ 可以匹配连续出现的若干个”ab”字符串,但不会将每个”ab”作为一个独立的捕获组。举例如下:
“abc”.match(/(?:ab)c+/) // 返回 [‘abc]
只返回了整体的匹配,没有返回分组”ab”。 - 5 命名捕获组: 在 ES2018 引入的正则表达式命名捕获组语法中,通过在正则表达式中使用语法 (?pattern) 来定义命名捕获组,可以在执行匹配时通过 name 获取到该组捕获的内容。后边正则表达式的 exec 方法章节再详细讲解如何获取到命名捕获组的内容。
反斜杠\
反斜杠在正则表达式中也属于一个特殊的字符,正斜杠是/,可以记住正斜杠是从下到上,反斜杠是从上到下,它在正则表达式中有一些常见用法包括:
-
转义特殊字符:从以上章节的学习中,我们可以看到,在正则表达式中,有很多的特殊字符,比如 . () {} [] _ ? | 等等, 那么如果想要匹配正则表达式中的特殊字符本身,就可以使用反斜杠进行转义,语法就是在特殊字符前加上反斜杠。例如,如果想要匹配一个点号”.”,可以使用正则表达式”.”,想要匹配一个点号”_”,可以使用正则表达式”*”,以此类推。
-
表示特殊字符类别:在反斜杠后面加上特殊的字符,可以表示特定的字符类别,这种形式也叫预定义模式(Predefined Patterns)是指一些常见模式的简写方式,可以帮助我们更快速、更方便地编写正则表达式。常见的情况如下:
\d:表示任意数字字符,等价于[0-9]。
\D:表示任意非数字字符,等价于[^0-9]。
\s:表示任意空白字符,包括空格、制表符、换行符等。
\S:表示任意非空白字符。
\w:表示任意字母、数字或下划线字符,等价于[A-Za-z0-9_]。
\W:表示任意非字母、数字或下划线字符,等价于[^A-Za-z0-9_]。
\b:表示单词边界。
\B:表示非单词边界。
\t:表示制表符。
\r:表示回车符。
\n:表示换行符。
\f:表示换页符。
有时候在正则表达式中需要匹配任意字符,包括换行符等。此时可以使用字符集合来表示任意字符,例如 [\s\S]、[\d\D]、[\w\W] 等,它们都表示匹配任意字符。这种方式在一些情况下比直接使用 “.” 更加可靠,因为有些正则表达式引擎默认不会将 “.” 匹配换行符等特殊字符,而使用字符集合则可以覆盖所有字符。 -
匹配特定字符集:反斜杠也可以用于匹配特定的字符集,例如”\p{L}”表示任意 Unicode 字母字符,”\p{P}”表示任意 Unicode 标点符号字符。
断言
正则表达式中的断言(Assertion)是在匹配的时候,加了对匹配内容前后的条件,比如前面必须匹配什么,后边必须匹配什么,前面不能匹配什么,后面不能匹配什么。具体来说,有以下四种类型的断言:
正向先行断言(Positive Lookahead Assertion):
语法为 “(?=pattern)” , 匹配的内容必须“先行”在” 断言”的前面 ,”x(?=pattern)”匹配的语义是,x 只有在 pattern 的前面才能匹配,注意 pattern 只是断言的条件,不在最后匹配的结果中,匹配的结果只有 x。
举个例子,假设有一个字符串”good food and good water”,我们想要匹配 “good” 这个单词,但是只在 “good” 后面紧跟着 ” food” 时才匹配。这个时候,我们可以使用正向先行断言来进行匹配,正则表达式为 “good(?= food)”。
在这个例子中,”(?= food)” 表示匹配 “good” 之后紧接着的字符串为 ” food”。正则表达式引擎会在字符串中逐个字符进行匹配,当匹配到 “good” 时,会继续向后查找,如果发现后面紧跟着 ” food”,则认为匹配成功。执行结果如下:
“good food and good water”.match(/good(?= food)/g) // 返回 [‘good’]
看到只匹配了一个 good,而没有匹配后边的 good,并且只匹配了 good 没有匹配” food”。
负向先行断言(Negative Lookahead Assertion):
语法为 ” (?!pattern)” , 匹配的内容不能“先行”在“断言”的前面 ,”x(?!=pattern)”匹配的语义是,x 不能在 pattern 的前面才能匹配,注意 pattern 只是断言的条件,不在最后匹配的结果中,匹配的结果只有 x。例如,正则表达式 “a(?!b)” 表示匹配所有后面不跟着 “b” 的 “a”。在字符串 “acab” 中,该正则表达式可以匹配到第一个字符 “a”,但不能匹配到 “ab”,因为 “b” 跟在了 “a” 的后面。执行结果如下:
“abac”.match(/a(?!b)/g) // 返回 [‘a’]
正向后行断言(Positive Lookbehind Assertion):
语法为 ” (?<=pattern)” , 匹配的内容必须“后行”在“断言”的后面 ,” (?<=pattern) x “匹配的语义是,x 必须在 pattern 的后面才能匹配,注意 pattern 只是断言的条件,不在最后匹配的结果中,匹配的结果只有 x。例如,正则表达式 “(?<=a)b” 表示匹配所有前面跟着 “a” 的 “b”。在字符串 “cbab” 中,该正则表达式可以匹配到第二个字符 “b”,但不能匹配到 “cb”。 执行结果如下:
“cbab”.match(/(?<=a)b/g) // 返回 [‘b’]
负向后行断言(Negative Lookbehind Assertion):
语法为 ” (?<=pattern)” , 匹配的内容必须“后行”在“断言”的后面 ,” (?<=pattern) x “匹配的语义是,x 必须在 pattern 的后面才能匹配,注意 pattern 只是断言的条件,不在最后匹配的结果中,匹配的结果只有 x。例如,正则表达式 “(?<=a)b” 表示匹配所有前面跟着 “a” 的 “b”。在字符串 “cbab” 中,该正则表达式可以匹配到第二个字符 “b”,但不能匹配到 “cb”。 执行结果如下:
RegExp 的实例属性
RegExp 常用的实例属性包括:
global:一个布尔值,表示是否开启全局匹配模式。对应的修饰符是 g。
ignoreCase:一个布尔值,表示是否开启忽略大小写匹配模式。对应的修饰符是 i。
multiline:一个布尔值,表示是否开启多行匹配模式。对应的修饰符是 m。
dotAll:一个布尔值,表示是否开启 “.” 匹配任意字符(包括换行符)的模式。在 ES2018 中引入,有些浏览器可能不支持。对应的修饰符是 s。
unicode:一个布尔值,表示是否开启 Unicode 字符串匹配模式。在 ES6 中引入,有些浏览器可能不支持。对应的修饰符是 u。
sticky:一个布尔值,表示是否开启粘连匹配模式。在 ES2015 中引入,有些浏览器可能不支持。对应的修饰符是 y。
这些属性都是只读的,可以通过正则表达式实例对象的属性访问器来获取它们的值。例如:
const regex = /test/gimsuy;
console.log(regex.global); // true
console.log(regex.ignoreCase); // true
console.log(regex.multiline); // true
console.log(regex.dotAll); // true
console.log(regex.unicode); // true
console.log(regex.sticky); // true
const regexdefault = /test/;
console.log(regexdefault.global); // false
console.log(regexdefault.ignoreCase); // false
console.log(regexdefault.multiline); // false
console.log(regexdefault.dotAll); // false
console.log(regexdefault.unicode); // false
console.log(regexdefault.sticky); // false
source: 也是一个只读属性,返回一个字符串,表示当前正则表达式的源代码。
例如,对于正则表达式 /[a-z]+/gi,其 source 属性返回的值就是字符串 [a-z]+。这个属性在某些场合下可以用来动态地获取当前正则表达式的源代码。
示例代码:
const regex = /[a-z]+/gi;
console.log(regex.source); // 输出 [a-z]+
注意因为反斜杠在字符串中也是转义字符,如果要在字符串中表示一个反斜杠字符本身,则需要使用 \ 表示,所以对于正则表达式中的反斜杠\,在转换为字符串形式时中需要对反斜杠再进行一次转义,变成了\。实例代码如下:
const regex = /[a-z]+\d/gi;
console.log(regex.source); // 实际字符串 [a-z]+\\d 但是打印出来是 输出 [a-z]+\d,因为打印的时候\\d转义为了\d
lastIndex: 是一个可读写的整数属性,用于设置或获取下一次匹配的起始位置。当使用带有 g 全局修饰符的正则表达式进行匹配时,每次匹配都会从 lastIndex 属性的值开始。如果匹配成功,lastIndex 属性的值会被设置为匹配文本的下一个位置,否则 lastIndex 会被重置为 0。注意,在不使用全局修饰符的情况下,对 lastIndex 的设置是无效的。
下面是一个例子,展示了 RegExp.prototype.lastIndex 的使用:
const regex = /hello/g;
const str = 'hello world hello';
console.log(regex.lastIndex); // 0 从0开始
regex.test(str); // true
console.log(regex.lastIndex); // 5 下次从5开始
regex.test(str); // true
console.log(regex.lastIndex); // 17 下次从17开始
regex.test(str); // false
console.log(regex.lastIndex); // 0 没有匹配 从0开始
regex.lastIndex = 6; // 设置成从6开始
regex.test(str); // true
console.log(regex.lastIndex); // 17 下次从17开始
在这个例子中,正则表达式 /hello/g 带有全局修饰符,所以 regex.test(str) 会从 lastIndex 的值开始进行匹配。一开始 lastIndex 的值为 0,所以第一次匹配成功后会将 lastIndex 设置为 5,下一次匹配从第索引是 5 的位置开始。从索引是 17 的地方开始,没有匹配到,所以 lastIndex 重置为 0。又可以指定 lastIndex 的位置为索引 6 开始。
RegExp 的实例方法
RegExp.prototype.test()
RegExp.prototype.test() 方法用于测试字符串是否匹配正则表达式。该方法接收一个字符串参数,返回一个布尔值。如果字符串与正则表达式匹配,则返回 true,否则返回 false。当正则表达式使用带有 g 全局修饰符的正则表达式进行匹配时,该方法会自动更新 lastIndex 属性,以记录下一次匹配的起始位置。在使用 test() 方法前,需要确保 lastIndex 属性的值是正确的。如果想要用正则表达式对不同的字符串进行调用时,记得需要在每次调用前将 lastIndex 属性重置为 0,以避免出现错误。
下面是一个使用 test() 方法的例子:
const regex = /foo/g;
const str1 = 'foobar';
const str2 = 'foobar';
console.log(regex.test(str1)); // true
console.log(regex.lastIndex); // 3
console.log(regex.test(str2)); // false
在这个例子中,正则表达式 /foo/g 先匹配 str1,返回 true,但是 lastIndex 已经是 3 了,在没有改变它的情况下,继续匹配 str2,将会从 str2 索引是 3 的地方开始,所以匹配失败。
需要注意的是,test() 方法只会返回一个布尔值,而不能返回所有匹配的结果。
RegExp.prototype.exec()
RegExp.prototype.exec() 方法用于在字符串中查找匹配的方法,返回一个数组或者 null。
它的语法如下:
regexp.exec(str)
其中,regexp 是一个正则表达式对象,str 是要在其中查找匹配的字符串。
如果没有找到匹配项,则返回 null。
如果匹配成功,则 exec() 返回一个数组,数组的第一个元素是匹配到的子字符串,如果正则表达式中有捕获组,则后边的元素是匹配到的捕获组元素。如果没有捕获组则数组只有一个元素。 返回的数组对象上还有 3 个属性:
input: 是原始输入的字符串。
index: 是匹配子字符串在原始输入字符串中的起始索引。
groups: 如果正则表达式中没有命名捕获组,则这个属性是 undefined, 如果有,则该属性是一个对象,其属性名是命名捕获组的名称,属性值是相应的捕获组捕获到的字符串。
如果正则表达式使用了全局标志 g,则 exec()在每次运行时都会查找下一个匹配项,并且它会自动更新 lastIndex 属性以反映匹配项的结束位置。如果未使用全局标志,则 exec()将始终返回与完全匹配的第一个匹配项相关的信息。
举个例子,假如要查找字符串 “hello world” 中的所有单词,可以使用如下代码:
const re = /\b\w+\b/g;
const str = 'hello world';
let match;
while ((match = re.exec(str)) !== null) {
console.log(match);
}
循环中会执行 2 次输出:
//第一次 输出
['hello', index: 0, input: 'hello world', groups: undefined]
// 第二次输出
['world', index: 6, input: 'hello world', groups: undefined]
第一次匹配和 hello,第二次匹配了 world,可以通过 index 看到他们在原始字符串中的起始位置,groups 都为 undefined,因为/\b\w+\b/g 中没有捕获组。
再看一个有捕获组的例子:
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const str = '2022-04-08';
console.log(re.exec(str));
输出结果如下:
['2022-04-08', '2022', '04', '08', index: 0, input: '2022-04-08', groups: {
day: "08",
month: "04",
year: "2022"
}]
这个例子是定义了 3 个命名捕获组,所以数组会有四个值,同时 groups 也成为了一个 key 值为捕获组名称,value 为捕获组内容的对象。
String 中使用到正则表达式的方法
String.prototype.split
它的函数签名如下:
str.split(separator, limit)
其中,str 为要拆分的字符串,separator 是指定的分隔符,可以是字符串或者正则表达式。limit 是一个可选的参数,指定拆分的数组长度上限。函数的返回值是一个数组,包含拆分后的字符串。String 章节讲了 separator 是字符串的情况,本章就继续讲 separator 是正则表达式的情况。看一个通过正则表达式拆分字符串为数组的例子:
const str = 'JavaScript, Python, Java, Ruby';
const result = str.split(/,\s*/);
console.log(result); // ["JavaScript", "Python", "Java", "Ruby"]
注意: 正则表达式加上或不加 g 修饰符对于 split() 方法的行为没有影响,
因为该方法都会匹配所有符合条件的位置并将其拆分成数组。
在使用 split 方法时,如果正则表达式中包含捕获组,则捕获组匹配到的内容也会包含在数组中。例如,如果我们想要将一个字符串按照逗号和空格进行分割,可以使用正则表达式/,\s*/,其中 \s* 匹配零个或多个空格。使用这个正则表达式进行 split 操作会得到一个数组,数组中包含了字符串被分割后的各个部分。例如:
const str = 'a, b, c, d';
const arr = str.split(/,\s*/);
console.log(arr); // ['a', 'b', 'c', 'd']
在这个例子中,由于正则表达式中没有括号,因此返回的数组只包含被分割的部分,不包含分隔符。
如果我们想要在分割的同时保留分隔符,可以使用括号来捕获分隔符,例如:
const str = 'a, b, c, d';
const arr = str.split(/(,\s*)/);
console.log(arr); // ['a', ', ', 'b', ', ', 'c', ', ', 'd']
在这个例子中,正则表达式中的 (,\s*) 表示捕获一个逗号和零个或多个空格,捕获的内容也会作为数组的成员返回。因此,返回的数组中既包含被分割的部分,也包含分隔符。
String.prototype.search
String.prototype.search() 方法执行一个正则表达式匹配检索,返回第一个匹配到的子串的位置(索引),如果没有找到匹配的子串,则返回 -1。
函数签名如下:
str.search(regexp)
其中,str 是待搜索的字符串,regexp 是一个正则表达式。
举个例子:
const str = 'Hello, world!';
const result = str.search(/world/);
console.log(result); // 输出 7
在上面的例子中,正则表达式 /world/ 匹配了字符串 str 中的 “world” 子串,因此 search() 方法返回了该子串的位置,即 7。
如果正则表达式没有匹配到任何子串,那么 search() 方法会返回 -1。例如:
const str = 'Hello, world!';
const result = str.search(/foo/);
console.log(result); // 输出 -1
String.prototype.match
String.prototype.match 用于在字符串中搜索指定的正则表达式。
函数签名如下:
match(regexp: RegExp | string): RegExpMatchArray | null;
其中,regexp 参数可以是一个 RegExp 对象或者一个字符串。如果它是一个字符串,则会将其隐式转换为 RegExp 对象。
在匹配失败时,它的返回值是 null,在匹配成功时,它的返回值
还和正则表达式是否有 g 修饰符有关系,当有 g 修饰符的时候,返回的就是一个普通的数组,包含所有匹配到的字符串;当没有 g 修饰符的时候, 它的返回值和 RexExp.prototype.exec 方法的返回值一样,返回的数组的第一个元素是匹配到的子字符串,如果正则表达式中有捕获组,则后边的元素是匹配到的捕获组元素,返回的数组对象上还有 3 个属性:
input: 是原始输入的字符串。
index: 是匹配子字符串在原始输入字符串中的起始索引。
groups:和 RexExp.prototype.exec 不同的是,即使有命名捕获组,它还一直是 undefined。看下边的例子:
const str = 'hello world';
const regex1 = /(\w+)\s(\w+)/;
const regex2 = /(\w+)\s(\w+)/g;
console.log(str.match(regex1)); // ["hello world", "hello", "world"]
console.log(str.match(regex2)); // ["hello world"]
在第一个例子中,正则表达式 regex1 没有 g 修饰符,返回的结果是一个数组,第一个元素是匹配到的整个字符串,接下来的两个元素是两个分组匹配到的内容。在第二个例子中,正则表达式 regex2 带有 g 修饰符,返回的结果是一个数组,其中只有一个元素,也就是匹配到的整个字符串。
再来看下有命名捕获组的情况和 RexExp.prototype.exec 相同的例子:
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const str = '2022-04-08';
console.log(str.match(str)); // 输出 ['2022-04-08', index: 0, input: '2022-04-08', groups: undefined]
但是和 exec 方法不同的是,match 方法不会识别命名捕获组。
String.prototype.replace
String.prototype.replace() 方法用于将匹配一个正则表达式的子串替换为一个新的字符串,并返回替换后的新字符串。
函数签名如下:
str.replace(regexp|substr, newSubstr|function)
其中,第一个参数可以是一个正则表达式或一个字符串,第二个参数可以是一个字符串或一个函数。
如果第一个参数是字符串,则只会替换第一个匹配到的子串,如果第一个参数是正则表达式,并且设置了 g 修饰符,则会将字符串中所有匹配正则表达式的子串都替换为第二个参数中指定的新字符串。看下边的例子:
const str = 'hello world';
const newStr = str.replace('o', '0');
console.log(newStr); // 'hell0 world'
const regExp = /o/;
const newStr2 = str.replace(regExp, '0');
console.log(newStr2); // 'hell0 world'
const regExp2 = /o/g;
const newStr3 = str.replace(regExp2, '0');
console.log(newStr3); // 'hell0 w0rld'
当第二个参数是 function 时,它的函数签名如下:
function (match, p1, p2, ..., offset, string) {}
其中,match 是匹配到的子串,p1、p2 等表示括号捕获的分组,offset 是匹配到的子串在原字符串中的偏移量,string 是原字符串。该函数返回值将作为替换后的新字符串,注意一定要加返回值,否则将被替换成 undefined。
注意,match 与分组参数(即 p1、p2 等)的个数取决于正则表达式中有多少个分组,因此函数的参数个数也取决于正则表达式中的分组数量。例如,如果正则表达式中有两个分组,则函数应该有 5 个参数:match、p1、p2,以及 offset 和原字符串。看下面的例子:
例 1 说明第一个参数时字符串的时候,第二个参数也可以是函数。
var result = '1111'.replace('1', function (...args) {
console.log(args); // ['1', 0, '1111']
return 2;
});
console.log(result); // 2111
例 2 函数中打印了 4 次,说明每次匹配都会执行一遍函数,最后把所有匹配项替换。
var result = '1111'.replace(/1/g, function (...args) {
console.log(args); // ['1', 0, '1111'],['1', 1, '1111'],['1', 2, '1111'],['1', 3, '1111']
return 2;
});
console.log(result); // "2222"
例 3 说明有捕获组的情况,捕获组的内容会作为参数。
const str = 'hello world';
const newStr = str.replace(/(\w+)\s(\w+)/, (...args) => {
console.log(args); // ['hello world', 'hello', 'world', 0, 'hello world']
return `${args[2]} ${args[1]}`;
});
console.log(newStr); // world hello
例 2 看一下有命名捕获组的情况,竟然会给函数再增加一个参数,参数值是一个对象,key 值是分组名,value 值是分组值。
const str = 'hello world';
const newStr = str.replace(/(?<name>\w+)\s(?<name2>\w+)/, (...args) => {
console.log(args); // ['hello world', 'hello', 'world', 0, 'hello world', {name: "hello", name2: "world"}]
return `${args[2]} ${args[1]}`;
});
console.log(newStr); // world hello