正则表达式在开发中很常用,但是很多人一般不会去纠结这玩意到底怎么写,大部分人呢都是从网上找一个符合要求的写上去就行。
这次就来详细的探讨一下正则表达式到底该如何写,有哪些坑点。我看网上很多的文章都是讲解正则表达式有哪些字符,但是不去讲解它的相关属性方法,这就导致很多人在实操的时候会出现一些大大小小的问题,然后又去查半天的资料,结果是相关属性方法的问题,白白浪费大半天时间。
一、正则表达式的创建方式
在JavaScript中,创建正则表达式有两种方式:字面量方式 和构造函数方式,两者的使用场景略有不同。
1. 字面量方式(推荐)
用 /正则表达式/修饰符
的格式创建,适用于正则规则固定的场景。
javascript
// 匹配字符串中的"cat"(忽略大小写)
const reg1 = /cat/i;
2. 构造函数方式
用 new RegExp(规则字符串, 修饰符)
创建,适用于正则规则需要动态生成的场景(如规则包含变量)。
javascript
// 动态生成匹配规则:匹配变量word的值
const word = "cat";
const reg2 = new RegExp(word, "i"); // 等价于 /cat/i
两种方式的区别
特性 | 字面量方式 | 构造函数方式 |
---|---|---|
语法格式 | /pattern/flags |
new RegExp(pattern, flags) |
转义字符 | 需转义1次(如 /\d/ ) |
需转义2次(如 new RegExp("\\d") ) |
动态规则 | 不支持(规则固定) | 支持(规则可含变量) |
性能 | 解析时缓存,性能略优 | 每次创建都重新解析 |
三、正则表达式的核心语法
正则的语法由「原子」「量词」「边界符」「修饰符」「分组与引用」等部分组成,我们逐一拆解。
1. 原子:正则的"最小匹配单位"
原子是正则中不可再分割的基本单位,用于匹配单个字符,主要分为3类:
(1)普通原子
直接匹配字符本身,如字母(a-z
)、数字(0-9
)、符号(!@#
)等。
- 示例:
/abc/
匹配字符串中的"abc"(如"abc123"会匹配"abc")。
(2)特殊原子(元字符)
具有特殊含义的字符,需牢记常用元字符:
元字符 | 含义 | 示例 |
---|---|---|
. |
匹配除换行符(\n )外的任意字符 |
/a.b/ 匹配"aab""acb" |
\d |
匹配任意数字(等价于 [0-9] ) |
/\d{3}/ 匹配"123" |
\D |
匹配非数字(等价于 [^0-9] ) |
/\D/ 匹配"a""#" |
\w |
匹配字母、数字、下划线(等价于 [a-zA-Z0-9_] ) |
/\w/ 匹配"a""5""_" |
\W |
匹配非字母、数字、下划线 | /\W/ 匹配"@""空格" |
\s |
匹配空白字符(空格、制表符\t 、换行符\n 等) |
/a\sb/ 匹配"a b" |
\S |
匹配非空白字符 | /\S/ 匹配"a""1" |
\n |
匹配换行符 | /a\nb/ 匹配"a换行b" |
\t |
匹配制表符 | /a\tb/ 匹配"a b" |
(3)自定义原子(字符组)
用 []
包裹多个字符,匹配其中任意一个字符,支持范围表示。
- 示例:
[abc]
匹配"a""b"或"c"(等价于a|b|c
);[a-z]
匹配任意小写字母;[A-Za-z0-9]
匹配任意字母或数字(等价于\w
去掉下划线);[^abc]
匹配除"a""b""c"外的任意字符(^
在字符组中表示"排除")。
2. 量词:控制原子的匹配次数
量词用于指定前面的原子(或原子组)需要匹配的次数,常用量词如下:
量词 | 含义 | 示例 |
---|---|---|
* |
匹配0次或多次(等价于 {0,} ) |
/a*/ 匹配"""a""aa" |
+ |
匹配1次或多次(等价于 {1,} ) |
/a+/ 匹配"a""aa" |
? |
匹配0次或1次(等价于 {0,1} ) |
/a?/ 匹配"""a" |
{n} |
精确匹配n次 | /a{3}/ 匹配"aaa" |
{n,} |
匹配n次或更多次 | /a{2,}/ 匹配"aa""aaa" |
{n,m} |
匹配n到m次(含n和m) | /a{2,4}/ 匹配"aa""aaa""aaaa" |
贪婪与非贪婪模式(重点)
默认情况下,量词是贪婪模式 ------即尽可能多地匹配字符;在量词后加 ?
可切换为非贪婪模式------即尽可能少地匹配字符。
- 示例:用
/a.*b/
(贪婪)和/a.*?b/
(非贪婪)匹配字符串"aabab"- 贪婪模式:
a.*b
会从第一个"a"匹配到最后一个"b",结果为"aabab"; - 非贪婪模式:
a.*?b
会从第一个"a"匹配到最近的"b",结果为"aab"。
- 贪婪模式:
这是正则中最容易踩坑的点之一,比如提取HTML标签时,贪婪模式可能会匹配到多余的内容。
3. 边界符:控制匹配的位置
边界符用于指定匹配的"位置"(而非字符),常用边界符如下:
边界符 | 含义 | 示例 |
---|---|---|
^ |
匹配字符串的开头(多行模式下匹配每行开头) | /^abc/ 匹配"abc123"中的"abc",不匹配"123abc" |
$ |
匹配字符串的结尾(多行模式下匹配每行结尾) | /abc$/ 匹配"123abc"中的"abc",不匹配"abc123" |
\b |
匹配单词边界(单词与非单词的交界处) | /\bcat\b/ 匹配"cat""cat123""123cat",不匹配"category" |
\B |
匹配非单词边界 | /\Bcat\B/ 匹配"category"中的"cat",不匹配"cat123" |
- 常见误区:
^
和$
在默认模式(单行模式)下只匹配整个字符串的开头和结尾;若开启多行模式(修饰符m
),则会匹配每一行的开头和结尾。
4. 修饰符:调整正则的匹配规则
修饰符用于全局调整正则的匹配行为,写在正则的末尾(字面量方式)或构造函数的第二个参数(构造函数方式),常用修饰符如下:
修饰符 | 含义 | 示例 |
---|---|---|
i |
忽略大小写(ignore case) | /cat/i 匹配"cat""Cat""CAT" |
g |
全局匹配(global) | /a/g 匹配字符串中所有的"a" |
m |
多行模式(multiline) | /^a/m 匹配"a\nabc"中的两个"a"(每行开头) |
s |
单行模式(dotall):让 . 匹配换行符 |
/a.b/s 匹配"a\nb" |
u |
Unicode模式:支持匹配Unicode字符(如中文、 emoji) | /[\u4e00-\u9fa5]/u 匹配中文 |
y |
粘性匹配(sticky):只从 lastIndex 位置开始匹配 |
见下文属性部分 |
5. 分组与引用:处理复杂匹配
当需要将多个原子视为一个整体(或捕获匹配结果)时,需要用到分组与引用。
(1)普通分组(()
)
用 ()
将多个原子包裹,视为一个"原子组",可配合量词使用。
- 示例:
/(ab)+/
匹配"ab""abab""ababab"(将"ab"视为一个整体,匹配1次或多次)。
(2)捕获分组与引用
普通分组会"捕获"匹配到的内容,后续可通过「反向引用」或「正则方法」获取捕获结果:
-
反向引用:用
\n
(n为分组序号,从1开始)引用前面捕获的内容; -
示例:匹配重复的两个字符,如"aa""bb""cc":
javascriptconst reg = /(\w)\1/; console.log(reg.test("aa")); // true console.log(reg.test("ab")); // false
-
捕获结果获取:通过
exec()
或match()
方法获取分组捕获的内容(见下文方法部分)。
(3)非捕获分组((?:)
)
若只需将多个原子视为整体,无需捕获结果,可用 (?:)
减少性能开销。
- 示例:
/(?:ab)+/
匹配"ab""abab",但不会捕获"ab"(比普通分组更高效)。
(4)正向预查与负向预查(断言)
预查(断言)用于判断"当前位置的前后是否符合特定规则",但不捕获结果(零宽度匹配),分为4种:
预查类型 | 语法 | 含义 | 示例 |
---|---|---|---|
正向肯定预查 | (?=pattern) |
后面必须匹配pattern | /a(?=b)/ 匹配"ab"中的"a",不匹配"ac"中的"a" |
正向否定预查 | (?!pattern) |
后面不能匹配pattern | /a(?!b)/ 匹配"ac"中的"a",不匹配"ab"中的"a" |
反向肯定预查 | (?<=pattern) |
前面必须匹配pattern | /(?<=a)b/ 匹配"ab"中的"b",不匹配"cb"中的"b" |
反向否定预查 | (?<!pattern) |
前面不能匹配pattern | /(?<!a)b/ 匹配"cb"中的"b",不匹配"ab"中的"b" |
-
实战场景:给手机号中间4位加*(如"138****1234"):
javascriptconst phone = "13812341234"; const result = phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2"); console.log(result); // "138****1234"
这里
(\d{3})
和(\d{4})
是捕获分组,$1
和$2
引用分组结果。
四、正则表达式的属性
正则对象(RegExp
实例)有一些内置属性,用于获取正则的配置或状态,常用属性如下:
属性名 | 含义 | 只读/可写 | 示例 |
---|---|---|---|
source |
返回正则的"规则字符串"(不含修饰符) | 只读 | /cat/i.source → "cat" |
global |
判断是否有 g 修饰符(全局匹配) |
只读 | /cat/g.global → true |
ignoreCase |
判断是否有 i 修饰符(忽略大小写) |
只读 | /cat/i.ignoreCase → true |
multiline |
判断是否有 m 修饰符(多行模式) |
只读 | /^cat/m.multiline → true |
lastIndex |
下一次匹配的起始位置(仅 g 或 y 修饰符生效) |
可写 | 见下文示例 |
sticky |
判断是否有 y 修饰符(粘性匹配) |
只读 | /cat/y.sticky → true |
重点属性:lastIndex
这玩意很重要,反正我是被这玩意坑过😓,我去学正则表达式的属性也是因为这玩意。
lastIndex
是正则中最特殊的属性,仅在正则有 g
(全局匹配)或 y
(粘性匹配)修饰符时生效,用于记录"下一次匹配的起始位置"。就是这个下一次匹配的起始位置 ,就可能导致你的匹配结果出错,要重点注意。
示例1:g
修饰符与 lastIndex
javascript
const reg = /a/g;
const str = "aaa";
// 第一次匹配:从 index 0 开始,匹配到 "a",lastIndex 变为 1
console.log(reg.exec(str)); // ["a", index: 0]
console.log(reg.lastIndex); // 1
// 第二次匹配:从 index 1 开始,匹配到 "a",lastIndex 变为 2
console.log(reg.exec(str)); // ["a", index: 1]
console.log(reg.lastIndex); // 2
// 第三次匹配:从 index 2 开始,匹配到 "a",lastIndex 变为 3
console.log(reg.exec(str)); // ["a", index: 2]
console.log(reg.lastIndex); // 3
// 第四次匹配:从 index 3 开始,无匹配,lastIndex 重置为 0
console.log(reg.exec(str)); // null
console.log(reg.lastIndex); // 0
示例2:y
修饰符与 lastIndex
(粘性匹配)
y
修饰符比 g
更严格------仅从 lastIndex
位置开始匹配,若该位置不匹配则直接返回 null
(g
会继续向后查找)。
javascript
const regG = /a/g;
const regY = /a/y;
const str = "a1a2a3";
// g修饰符:从 index 0 匹配 "a",lastIndex 变为 1;下一次从 1 开始,跳过 "1" 匹配 index 2 的 "a"
console.log(regG.exec(str)); // ["a", index: 0]
console.log(regG.exec(str)); // ["a", index: 2]
// y修饰符:从 index 0 匹配 "a",lastIndex 变为 1;下一次从 1 开始(字符 "1"),不匹配,返回 null
console.log(regY.exec(str)); // ["a", index: 0]
console.log(regY.exec(str)); // null
五、正则表达式的常用方法
正则的方法分为两类:正则对象的方法 (test()
、exec()
)和字符串的方法 (match()
、replace()
等),我们重点讲解常用方法。
1. 正则对象的方法
(1)test(str)
:判断字符串是否匹配
-
作用:检查
str
是否符合正则规则,返回true
(匹配)或false
(不匹配)。 -
特点:
- 有
g
修饰符时,会更新lastIndex
(下次匹配从lastIndex
开始); - 无
g
修饰符时,lastIndex
始终为 0(每次都从开头匹配)。
- 有
-
示例:
javascriptconst reg = /cat/; console.log(reg.test("cat123")); // true console.log(reg.test("dog123")); // false
(2)exec(str)
:获取匹配结果(含分组)
-
作用:在
str
中查找符合正则的内容,返回一个数组(匹配成功)或null
(匹配失败)。 -
返回数组的结构:
- 索引 0:完整的匹配结果;
- 索引 1~n:各捕获分组的匹配结果;
index
:匹配结果在字符串中的起始位置;input
:原始字符串。
-
示例(含分组):
javascriptconst reg = /(\w+)@(\w+)\.(\w+)/; // 匹配邮箱,捕获用户名、域名、后缀 const str = "my email is test@example.com"; const result = reg.exec(str); console.log(result); // [ // "test@example.com", // 完整匹配结果(索引0) // "test", // 分组1:用户名(索引1) // "example", // 分组2:域名(索引2) // "com", // 分组3:后缀(索引3) // index: 12, // 匹配起始位置 // input: "my email is test@example.com" // 原始字符串 // ]
-
注意:有
g
修饰符时,可循环调用exec()
获取所有匹配结果(直到返回null
)。
2. 字符串的方法
(1)str.match(reg)
:获取匹配结果
-
作用:与
exec()
类似,但返回结果格式因reg
是否有g
修饰符而异:- 无
g
修饰符:返回与exec()
相同的数组(含分组、index、input); - 有
g
修饰符:返回一个数组,包含所有完整匹配结果(不含分组)。
- 无
-
示例:
javascriptconst reg1 = /(\w+)@(\w+)/; // 无g const reg2 = /(\w+)@(\w+)/g; // 有g const str = "a@b, c@d"; console.log(str.match(reg1)); // ["a@b", "a", "b", index: 0, input: "a@b, c@d"](含分组) console.log(str.match(reg2)); // ["a@b", "c@d"](仅完整匹配结果)
(2)str.replace(reg, replacement)
:替换匹配内容
-
作用:用
replacement
替换str
中符合reg
的内容,返回新字符串(不修改原字符串)。 -
replacement
的两种形式:- 字符串:可使用
$n
引用分组结果($0
是完整匹配,$1
是分组1,以此类推); - 函数:参数依次为"完整匹配结果、分组1、分组2、...、index、input",返回值为替换后的内容。
- 字符串:可使用
-
示例1:用字符串替换(手机号中间4位加*)
javascriptconst phone = "13812341234"; const newPhone = phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2"); console.log(newPhone); // "138****1234"
-
示例2:用函数替换(首字母大写)
javascriptconst str = "hello world"; const newStr = str.replace(/\b\w/g, (match) => match.toUpperCase()); console.log(newStr); // "Hello World"
(3)str.search(reg)
:查找匹配的起始位置
-
作用:返回
str
中第一个符合reg
的子串的起始位置,若不匹配则返回-1
。 -
特点:忽略
g
修饰符(始终只找第一个匹配),不更新lastIndex
。 -
示例:
javascriptconst str = "abc123abc"; console.log(str.search(/abc/)); // 0(第一个"abc"的起始位置) console.log(str.search(/123/)); // 3("123"的起始位置)
(4)str.split(reg)
:按匹配结果分割字符串
-
作用:用
reg
匹配的内容作为分隔符,将str
分割为数组,返回分割后的数组。 -
示例:按任意空白字符分割字符串
javascriptconst str = "a b\tc\nd"; // 包含空格、制表符、换行符 const arr = str.split(/\s+/); // 按1个或多个空白字符分割 console.log(arr); // ["a", "b", "c", "d"]
六、常见的正则表达式场景
掌握语法后,我们结合实际开发场景,看看正则如何解决问题。
1. 表单验证
(1)手机号验证(中国大陆手机号)
规则:11位数字,以13/14/15/17/18/19开头。
javascript
const reg = /^1[3-9]\d{9}$/;
console.log(reg.test("13812341234")); // true
console.log(reg.test("12345678901")); // false(开头不是13-19)
(2)邮箱验证(简化版)
规则:包含 @
和 .
,@
前有字符,@
和 .
之间有字符,.
后有2~6位字符。
javascript
const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+\.[a-zA-Z]{2,6}$/;
console.log(reg.test("test@example.com")); // true
console.log(reg.test("test@.com")); // false(@和.之间无字符)
(3)身份证号验证(18位)
规则:17位数字 + 1位数字/字母X(不区分大小写)。
javascript
const reg = /^\d{17}[\dXx]$/;
console.log(reg.test("110101199001011234")); // true
console.log(reg.test("11010119900101123X")); // true
2. 字符串处理
(1)提取URL中的参数
示例:从"https://example.com?name=张三\&age=20"中提取 name
和 age
的值。
javascript
const url = "https://example.com?name=张三&age=20";
const reg = /([^?&=]+)=([^&]+)/g; // 匹配"key=value"格式
const params = {};
let result;
while ((result = reg.exec(url)) !== null) {
const key = decodeURIComponent(result[1]); // 解码(处理中文)
const value = decodeURIComponent(result[2]);
params[key] = value;
}
console.log(params); // { name: "张三", age: "20" }
(2)替换敏感词
示例:将字符串中的"敏感词1""敏感词2"替换为"***"。
javascript
const str = "这是敏感词1,那是敏感词2";
const sensitiveWords = ["敏感词1", "敏感词2"];
const reg = new RegExp(sensitiveWords.join("|"), "g"); // 动态生成正则
const newStr = str.replace(reg, "***");
console.log(newStr); // "这是***,那是***"
(3)格式化日期
示例:将"20240520"格式化为"2024-05-20"。
javascript
const dateStr = "20240520";
const reg = /(\d{4})(\d{2})(\d{2})/;
const formattedDate = dateStr.replace(reg, "$1-$2-$3");
console.log(formattedDate); // "2024-05-20"
七、小结
总的来说,正则表达式并不难理解,就是它的字符有点多,但是我觉得这个是次要的,主要是重点理解它的属性,字符不记得直接查就能写出来,但是属性不记得的话,那错误一查就是大半天!!!!
参考资料
正则表达式:MDN官网------正则表达式
RegExp:MDN官网------RegExp