简介
以前写过一篇介绍正则的文章,以如今的眼光看来比较混乱,本篇在原有基础上进行精炼,使得正则知识点更加直观易懂
正则表达式(Regular Expression)是用于匹配字符串中字符组合的模式。在 JavaScript中,正则表达式也是对象,通常用来查找、替换那些符合正则表达式的文本
除了JavaScript,许多现代编程语言都内置了对正则表达式的支持。尽管不同语言在实现正则表达式功能时的具体语法、函数库或方法可能有所差异,但基本的正则表达式语法和元字符是通用的。这意味着,无论在哪种支持正则表达式的语言中,你都可以编写类似的正则模式来匹配特定的字符串结构
定义正则
JavaScript 中定义正则表达式的语法有两种:字面量、构造函数
字面量
使用 // 包裹的字面量创建方式是推荐的作法,但它不能在其中使用变量
// 正常定义并使用
/hello/.test('helloWorld') // true
// 不能在其中使用变量, 会当成字符串字面量
const str = 'hello'
// 此处 str 会被当成字符串而不是变量
/str/.test('helloWorld') // false
以上问题可以使用eval转换为 js 语法来实现将变量解析到正则中,但不推荐
const str = 'hello'
eval(`/${str}/`).test('helloWorld') // true
构造函数
当正则需要动态创建时使用对象方式,且能够使用变量
// new RegExp('表达式', '模式') 此处表达式也可以直接用/字面量/的形式
const str = 'helloWorld'
const reg = 'hello'
new RegExp(reg).test(str) // true
构造函数形式和字面量形式本质上没有区别,创建的都是RegExp对象。以下三种表达式都会创建相同的正则表达式:
/ab+c/i; //字面量形式
new RegExp("ab+c", "i"); // 首个参数为字符串模式的构造函数
new RegExp(/ab+c/, "i"); // 首个参数为常规字面量的构造函数
此外,当使用构造函数创造正则对象时,需要常规的字符转义规则(在前面加反斜杠 \)
new RegExp("\\w+") // 等价于 /\w+/
正则方法
test
判断是否有符合规则的字符串
- 如果正则表达式与指定的字符串匹配 ,返回
true,否则返回false
/hello/.test('helloWorld') // true
exec
检索符合规则的字符串
- 如果匹配成功,
exec()方法返回一个数组,否则返回null - 使用
g修饰符后可以循环调用直到全部匹配完
// 统计h字符出现的次数
const str = 'helloWorld,hahaha'
const reg = /h/g
let num = 0
while (reg.exec(str)) {
num ++
}
console.log(num) // 4
- 不使用
g修饰符,与match方法返回值相同
const str = 'helloWorld,hahaha'
const reg = /h/
console.log(reg.exec(str)) // [ 'h', index: 0, input: 'helloWorld,hahaha', groups: undefined ]
字符方法
search
检索与正则表达式相匹配的子字符串位置
- 找到则返回与指定正则表达式相匹配的索引起始位置,否则返回
-1
// 执行一次对大小写敏感的查找
let str="Mr. Blue has a blue house"
console.log(str.search("blue")) //15
match
找到一个或多个正则表达式的匹配
match()方法将检索字符串,以找到一个或多个与正则表达式匹配的文本。这个方法的行为在很大程度上有赖于正则表达式是否具有标志 g- 如果没有标志 g ,那么
match()方法就只能执行一次匹配,返回第一个完整匹配及其相关的捕获组(Array)如果没有找到任何匹配的文本,将返回 null
- 如果有 g 标志,则将返回与完整正则表达式匹配的所有结果,但不会返回捕获组
// 不使用 g
const str = "The quick brown fox jumps over the lazy dog"
const regex = /(\w+)\s(\w+)\s(\w+)/
const result = str.match(regex)
// result结果为
[
"The quick brown", // 整个匹配:与完整正则表达式匹配的子串
"The", // 第一个捕获组:第一个单词
"quick", // 第二个捕获组:第二个单词
"brown" // 第三个捕获组:第三个单词
index: 0, // 匹配开始的位置
input: "The quick brown fox jumps over the lazy dog", // 原始输入字符串
groups: undefined // 捕获组别名,这里没用别名所以为undefined
]
// 使用 g
let str="The rain in SPAIN stays mainly in the plain"
console.log(str.match(/ain/gi)) // [ 'ain', 'AIN', 'ain', 'ain' ]
split
以正则规则,把一个字符串分割成字符串数组
let str = "2024/04-15"
console.log(str.split(/-|\//)) // [ '2024', '04', '15' ]
replace
在字符串中用与正则表达式匹配的子串替换另一些字符
let str = "2024/04/15";
console.log(str.replace(/\//g, "-")); // 2024-04-15
返回值:一个新的字符串,其中一个、多个或所有的匹配项都被指定的替换项替换
替换字符串可以插入下面的特殊变量名:
| 变量 | 说明 |
|---|---|
| $$ | 插入一个 "$" |
| $& | 插入匹配的子串 |
| $` | 插入当前匹配的子串左边的内容 |
| $' | 插入当前匹配的子串右边的内容 |
| $n | 假如第一个参数是 RegExp 对象,并且 n 是个小于 100 的非负整数,那么插入第 n 个括号匹配的字符串(索引从 1 开始) |
// $&、$`、$'
let str = "~hello reg~";
console.log(str.replace(/hello reg/g, "$`$`$&$'$'"))
// $n
let tel = "(010)99999999 (020)8888888"
console.log(tel.replace(/\((\d{3,4})\)(\d{7,8})/g, "$1-$2"))
回调函数:replace 支持回调函数操作,用于处理复杂的替换逻辑
| 变量名 | 代表的值 |
|---|---|
match | 匹配的子串(对应于上述的$&) |
p1,p2, ... | 假如replace()方法的第一个参数是一个 RegExp 对象,则代表第 n 个括号匹配的字符串。(对应于上述的2 等。)例如,如果是用 /(\a+)(\b+)/ 这个来匹配,p1 就是匹配的 \a+,p2 就是匹配的 \b+。 |
offset | 匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是 'abcd',匹配到的子字符串是 'bc',那么这个参数将会是 1) |
string | 被匹配的原字符串 |
NamedCaptureGroup | 命名捕获组匹配的对象 |
const str = `<h1>hello</h1>
<h2>你好, 正则</h2>
<h1>reg</h1>`
let reg = /<(h[1-6])>([\s\S]*?)<\/\1>/gi;
const res = str.replace(
reg,
(
search, //匹配到的字符
p1, //第一个原子组
p2, //第二个原子组
index, //索引位置
source //原字符
) => {
return `<${p1} class="hot">${p2}</${p1}>`
}
)
console.log(res)
/*
<h1 class="hot">hello</h1>
<h2 class="hot">你好, 正则</h2>
<h1 class="hot">reg</h1>
*/
语法规则
可以将正则表达式的语法结构化,将正则表达式语法分为:
- 普通字符:明确的关键字
- 字符集合:关键字值的范围
- 限定符:给前一个字符追加出现次数范围
- 定位符:标记匹配位置的元字符
- 子表达式:内嵌的子正则表达式
- 省略符:为简化正则表达式的元字符
- 修饰符:指定匹配策略
- 断言:可以理解成自定义的定位符,用于精确控制匹配发生的位置
普通字符
大多数的字符仅能够描述它们本身,这些字符称作普通字符(如字母和数字)
普通字符只能够匹配字符串中与它们相同的字符,主要包括文字、符号、还有一些不常用的字符,如非打印字符、Unicode编码值等
由于部分字符在正则表达式中有特殊的含义,使用时需要转义\
| 使用时需要转义的符号字符 | 使用时需要转义的符号字符 |
|---|---|
. | * |
\ | + |
[ | ? |
] | { |
( | } |
) | | |
^ | $ |
字符集合
字符集合是单个字符的值范围,只要符合这个范围的字符都算是匹配成功。字符集合包含在[],在[]内,每一个字符都是允许匹配的值
从前往后匹配,若是全局匹配,则匹配完满足项后,会从满足项下一个开始匹配
[]匹配字符集合,用于匹配其中任意一个字符[]中的-连字符:表示一个范围[]中的^脱字符:表示不在中括号内的字符[]中除去连字符和脱字符外,其余大部分特殊字符全部当做普通字符
/[abc]/ // 匹配abc其中的任何单个字符
/[a-z]/ // 表示 a 到 z
/[a-zA-Z]/ // 表示大小写都可
/[0-9]/ // 表示 0~9 的数字都可
/[^a-z]/ // 匹配除了小写字母以外的字符
选择符:|,符合其中一个或以上就可以匹配
- 使用选择符可以起到字符集合的作用
/a|b|c/ // 匹配abc其中的任何单个字符
- 字符集合内的选择符没有
或的概念,只是单单表示|字符
const reg = /[a|b]/
const str = "|"
reg.test(str) // true
限定符(量词)
限定符是为了给前一个字符追加出现次数范围
| 限定符 | 说明 |
|---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次(逗号两侧不能有空格) |
{n,} | 重复n次或更多次(逗号两侧不能有空格) |
{n,m} | 重复n到m次(逗号两侧不能有空格) |
当限定符前面的字符是一个模糊的匹配范围,如一个字符集合,则会发生贪婪匹配的问题
贪婪匹配(greedy)会匹配到符合正则表达式匹配模式的字符串的最长可能部分,并将其作为匹配项返回
const reg = /t[a-z]*i/g
const str = 'titanic'
console.log(str.match(reg)) // [ 'titani' ]
可以在限定符后面添加?使其变成惰性匹配
惰性匹配(lazy),它会匹配到满足正则表达式的字符串的最小可能部分
调整后的正则表达式 /t[a-z]*?i/ 匹配字符串 "titanic" 返回 ["ti"]
// 匹配标签
let text = "<h1>Winter is coming</h1>"
let myRegex = /<.*?>/g
let result = text.match(myRegex) // 返回['<h1>', '</h1>']
// 匹配字符
const str = 'helloooo world'
const reg = /o+?/
console.log(str.match(reg)) // 'o'
const reg1 = /o+/
console.log(str.match(reg1)) // 'oooo'
定位符(边界符)
正则表达式中的定位符(边界符)用来提示字符所处的位置,主要有两个字符
| 定位符 | 说明 |
|---|---|
| ^ | 表示匹配行首的文本(以谁开始) |
| $ | 表示匹配行尾的文本(以谁结束) |
如果
^和$在一起,表示精确匹配
- 精确匹配:目标字符串必须与正则表达式完全一致,必须匹配整个字符串
- 非精确匹配:允许目标字符串在某些方面与正则表达式模式有所偏差,可能包含额外的字符,或者只匹配模式的一部分。当不使用
^和$时,或者使用了其他允许模糊匹配的元素(如字符集合、通配符、量词等),就可能出现非精确匹配
console.log(/^哈/.test('哈哈')) // true
console.log(/^哈/.test('二哈')) // flase
console.log(/^哈$/.test('哈')) // true
console.log(/^哈$/.test('哈哈')) // false
console.log(/^哈$/.test('二哈')) // false
// 易错点
const firstString = "Ricky is first and can be found."
const firstRegex = /^Ricky/
firstRegex.test(firstString) // true
const notFirst = "You can't find Ricky now."
firstRegex.test(notFirst) // false
子表达式(捕获组)
子表达式是内嵌的子正则表达式,子表达式写在()中,子表达式与正则表达式的语法相同,子表达式内可以内嵌子表达式
子表达式作用:匹配子表达式内容,匹配结果以编号或显示命名的方式存在内存,可供正则本身,也可供替换使用
- 子表达式可以看成一个整体
const str = '... got ... gotgot ...gt'
const reg = /(got)+/g
console.log(str.match(reg)) // [ 'got', 'gotgot' ]
- 子表达式可以作为多种情况的匹配范围,子表达式中用
|分割多个子表达式,以表示多种情况
const str = 'get a pet goat'
const reg = /g(e|oa)t/g
console.log(str.match(reg)) // [ 'get', 'goat' ]
- 子表达式也可以标记子匹配项,如需要匹配文本中
AABB形式的字符串,\1表示与第1个子匹配项相同的内容,\2表示与第2个子匹配项相同的内容
const str = '.... aabb ... bbcczz ... abcd'
const reg = /([a-z])\1([a-z])\2/g
console.log(str.match(reg)) // [ 'aabb', 'bbcc' ]
- 有的时候我们可能只想匹配分组,但是并不想缓存(不想捕获)匹配到的结果,就可以在我们的分组模式前面加上
?:
let reg = /(?:\d{2}).(\d{2}).(\d{4})/
let originString = '06-18-2023'
reg.test(originString) // true
originString.match(reg) // ['06-18-2023', '18', '2023']
match返回的是捕获组,还有其他属性,为方便查看这里只截取前半部分
由此我们可知,加入?:正则表达式依然是匹配的,但是$1不是06,而是18,因为我们在第一个括号里加了?:,06就不会被捕获。match()的执行结果会受?:的影响
match返回的捕获组,第一位为整个匹配,从第二位开始,才是第一个捕获组,依次类推,详情见本文讲解match部分
- 捕获组还可以起别名:
?<>,使用时通过$<别名>来使用
const str = `
<h1>hello</h1>
<span>你好</span>
<h2>world</h2>
`
const reg = /<(?<tag>h[1-6])>(?<con>[\s\S]*)<\/\1>/gi
console.log(str.replace(reg, `<p>$<con></p>`))
/*
<p>hello</p>
<span>你好</span>
<p>world</p>
*/
省略符
省略符是一些为了简化正则表达式而存在的元字符,一般以\开头
| 预定义类 | 说明 | 等效 |
|---|---|---|
\d | 匹配0-9之间的任一数字 | [0-9] |
\D | 匹配所有0-9以外的字符 | [^0-9] |
\w | 匹配任意的字母、数字和下划线 | [A-Za-zO-9 ] |
\W | 除所有字母、数字和下划线以外的字符 | [^A-Za-zO-9 ] |
\s | 匹配空格 (包括制表、回车、换行、垂直制表、换页、空格等) | [ \t\r\n\v\f] |
\S | 匹配非空格的字符 | [^ \t\r\n\v\f] |
. | 匹配除换行符外的任意字符 | [^\n\r] |
修饰符
正则表达式在执行时会按他们的默认执行方式进行,但有时候默认的处理方式总不能满足我们的需求,所以可以使用模式修饰符更改默认方式
| 修饰符 | 说明 |
|---|---|
i | 不区分大小写字母的匹配 |
g | 全局搜索所有匹配内容 |
m | 多行匹配 |
s | 视为单行忽略换行符,使用. 可以匹配所有字符 |
y | 从regexp.lastIndex开始匹配 |
u | 正确处理四个字符的UTF-16编码 |
i与g
console.log(/^java$/.test('javascript')) // true
console.log(/^java$/i.test('JAVASCRIPT')) // true
m:将内容视为多行匹配,主要是对^和$的修饰
const str = 'hello\n2world\n3hellow\n4JavaScript'
console.log(str.match(/^\d/)) // null
console.log(str.match(/^\d/m)) // 2
断言
断言可以理解为一种自定义的定位符。断言用于对匹配到的文本位置提出要求,不真正匹配文本内容本身,而是只负责位置匹配(如左边、右边、开始和结尾)这种结构使得我们可以更精确地控制匹配发生的位置
先了解下以下概念
- 零宽:只匹配位置,不占用字符
- 先行:正则引擎在扫描字符的时候,从左往右扫描,匹配扫描指针未扫描过的字符,先于指针,故称先行
- 后行:匹配指针已扫描过的字符,后于指针到达该字符,故称后行,即产生回溯
- 正向:匹配括号中的表达式
- 负向:不匹配括号中的表达式
断言只匹配某些位置,在匹配过程中,不占用字符,因此又被称为 "零宽",实际上,断言类似于定位符的作用
先行断言是告诉 JavaScript 在字符串中向前查找的匹配模式(从左到右扫描) 当想要在同一个字符串上搜寻多个匹配模式时,这可能非常有用
有两种先行断言:
- 正向先行断言:
(?=pattern),匹配后面为pattern的内容 - 负向先行断言:
(?!pattern),匹配后面不出现pattern的内容
const str = 'au bt'
const str1Regex = /[a-zA-Z](?=u)/
const str2Regex = /[a-zA-Z](?!u)/
console.log(str.match(str1Regex)) // ['a'],从左到右扫描,匹配第一个 u 前面的字母
console.log(str.match(str2Regex)) // ['u'],从左到右扫描,匹配第一个非 u 前面的字母
// 先行断言的更实际用途是检查一个字符串中的两个或更多匹配模式
// 这里有一个简单的密码检查器,密码规则是 3 到 6 个字符且至少包含一个数字
const password = "abc123"
const checkPass = /(?=\w{3,6})(?=\D*\d)/
checkPass.test(password)
// 在正则表达式 pwRegex 中使用先行断言以匹配大于 5 个字符且有两个连续数字的密码
const sampleWord = 'astronaut'
const pwRegex = /(?=\w{5,})(?=\D+\d{2})/
const result = pwRegex.test(sampleWord)
后行断言是针对扫描指针已扫描过的字符进行判断
有两种后行断言:
- 正向后行断言:
(?<=pattern),匹配前面为pattern的内容 - 负向后行断言:
(?<!pattern),匹配前面不出现pattern的内容
let str = 'ua bt'
let str1Regex = /(?<=u)[a-zA-Z]/
let str2Regex = /(?<!u)[a-zA-Z]/
console.log(str.match(str1Regex)) // ['a'],从左到右扫描,匹配第一个 u 后面的字母
console.log(str.match(str2Regex)) // ['u'],从左到右扫描,匹配第一个非 u 后面的字母
参考链接