JS 正则

一、创建正则

1
2
3
4
5
// 字面量(推荐,编译时创建)
const re1 = /pattern/flags;

// 构造函数(运行时创建,适用于动态模式)
const re2 = new RegExp('pattern', 'flags');

二、正则符号

字面字符

1
/abc/   匹配字符串中的 "abc"(精确匹配)

元字符

1
2
3
4
5
6
7
8
9
.       匹配除换行符外的任意单个字符
\d 匹配数字 [0-9]
\D 匹配非数字
\w 匹配字母、数字、下划线 [a-zA-Z0-9_]
\W 匹配非单词字符
\s 匹配空白符(空格、制表符、换行)
\S 匹配非空白符
\b 匹配单词边界
\B 匹配非单词边界

字符集合 []

1
2
3
4
[abc]       匹配 a、b、c 中的任意一个
[a-z] 匹配小写字母
[^abc] 匹配除 a、b、c 外的任意字符
[0-9a-f] 匹配十六进制数字

量词

1
2
3
4
5
6
*       零次或多次(等效 {0,})
+ 一次或多次(等效 {1,})
? 零次或一次(等效 {0,1})
{n} 恰好 n 次
{n,} 至少 n 次
{n,m} n 到 m 次

默认是贪婪匹配(尽可能多匹配)。量词后加 ? 变为非贪婪

1
2
"aaa".match(/a+/);      // ["aaa"](贪婪,取最多的)
"aaa".match(/a+?/); // ["a"](非贪婪,取最少的)

分组与引用 ()

1
2
3
(abc)       捕获组:匹配 abc 并捕获到组中供后续引用
(?:abc) 非捕获组:匹配 abc 但不捕获
\1, \2 反向引用:引用第 n 个捕获组匹配的内容
1
2
3
4
5
6
7
// 反向引用:匹配重复单词
/(\w+)\s+\1/.test('hello hello'); // true
/(\w+)\s+\1/.test('hello world'); // false

// 替换中的引用
'2024-03-15'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1');
// "03/15/2024"

锚点

1
2
3
4
5
6
^       字符串开头
$ 字符串结尾
(?=) 先行断言(后面必须跟指定模式)
(?!) 先行否定断言(后面不能跟指定模式)
(?<=) 后行断言(前面必须是指定模式)
(?<!) 后行否定断言(前面不能是指定模式)
1
2
3
4
5
6
7
8
9
10
11
12
/^hello/.test('hello world');     // true(以 hello 开头)
/world$/.test('hello world'); // true(以 world 结尾)

// 零宽断言
/\d+(?=px)/.test('16px'); // true(数字后面有 px)
/\d+(?!px)/.test('16px'); // false(数字后面是 px,不匹配)

/(?<=\$)\d+/.test('$100'); // true(数字前面是 $)
/(?<!\$)\d+/.test('$100'); // false(数字前面是 $,不匹配)

// 千分位
'12345678'.replace(/\d{1,3}(?=(\d{3})+$)/g, '$&,'); // "12,345,678"

修饰符

1
2
3
4
5
6
g   全局匹配(找到所有匹配,不 g 只找第一个)
i 忽略大小写
m 多行模式(^ 和 $ 匹配每行首尾)
s 点号通配(. 匹配换行符)
u Unicode 模式(正确处理四字节字符)
y 粘性匹配(从 lastIndex 处匹配)
1
2
/hello/gi.test('HELLO');      // true(忽略大小写)
"Line1\nLine2".match(/^\w+/gm); // ["Line1", "Line2"](多行模式每行匹配)

常用正则速查

1
2
3
4
5
6
7
8
/^[\w.-]+@[\w.-]+\.\w+$/        邮箱
/^1[3-9]\d{9}$/ 手机号(中国大陆)
/^https?:\/\/.+/ URL
/^\d{17}[\dXx]$/ 身份证号(18 位)
/^[a-zA-Z]\w{5,17}$/ 密码(字母开头,6-18 位)
/[\u4e00-\u9fa5]/ 中文字符
/\s+/ 空白字符串
/^\s+|\s+$/g 首尾空白

三、JS 中的正则 API

RegExp 上的方法

1
2
3
4
5
6
7
8
9
10
11
12
const re = /\d+/g;

// test:是否匹配(布尔)
re.test('abc123'); // true

// exec:匹配结果 + 索引 + 分组(非 g 模式每次返回相同结果,g 模式由 lastIndex 推进)
re.lastIndex; // 0
re.exec('abc123def456'); // ["123", index: 3, input: "abc123def456", groups: undefined]
re.lastIndex; // 6
re.exec('abc123def456'); // ["456", index: 9, ...]
re.lastIndex; // 12
re.exec('abc123def456'); // null(再无匹配,lastIndex 重置为 0)

String 上的正则方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const str = '2024-03-15';
const re = /(\d{4})-(\d{2})-(\d{2})/;

// match:检索匹配结果
str.match(re); // ["2024-03-15", "2024", "03", "15", index: 0, groups: undefined]
str.match(re)[1]; // "2024"(第一个捕获组)
str.match(/\d+/g); // ["2024", "03", "15"](全局模式下返回数组,不含捕获组信息)

// matchAll:返回迭代器(配合 g 模式,包含捕获组)
const matches = str.matchAll(/(\d+)/g);
[...matches].map(m => m[1]); // ["2024", "03", "15"]

// replace:替换
str.replace(re, '$2/$3/$1'); // "03/15/2024"
str.replace(/-/g, '/'); // "2024/03/15"

// replace 支持回调
'abc123def456'.replace(/\d+/g, (match, index, input) => {
return `[${match}]`;
}); // "abc[123]def[456]"

// search:返回匹配起始索引(类似 indexOf,但支持正则)
str.search(/-/); // 4

// split:分割
'2024-03-15'.split(/-/); // ["2024", "03", "15"]
'hello world'.split(/\s+/); // ["hello", "world"]

四、面试题

1. 千分位格式化

1
2
3
4
5
6
7
function toThousands(num) {
const parts = String(num).split('.');
parts[0] = parts[0].replace(/\d{1,3}(?=(\d{3})+$)/g, '$&,');
return parts.join('.');
}

toThousands(12345678.123); // "12,345,678.123"

2. 匹配手机号中间四位为 *

1
2
3
4
5
function maskPhone(phone) {
return String(phone).replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}

maskPhone('13812345678'); // "138****5678"

3. 模板字符串解析

1
2
3
4
5
6
function render(template, data) {
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => data[key] ?? match);
}

render('Hello {{name}}, you are {{age}}', { name: 'Alice', age: 25 });
// "Hello Alice, you are 25"

4. 驼峰与连字符互转

1
2
3
4
5
6
7
8
9
10
11
// 驼峰 → 连字符
function camelToKebab(str) {
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
}
camelToKebab('fontSize'); // "font-size"

// 连字符 → 驼峰
function kebabToCamel(str) {
return str.replace(/-(\w)/g, (_, c) => c.toUpperCase());
}
kebabToCamel('background-color'); // "backgroundColor"

5. 匹配 HTML 标签

1
2
3
4
5
6
7
8
// 匹配一对标签(含内容)
/<(\w+)[^>]*>.*?<\/\1>/gs.test('<div>text</div>'); // true

// 去除所有 HTML 标签
function stripTags(html) {
return html.replace(/<[^>]*>/g, '');
}
stripTags('<p>Hello <b>World</b></p>'); // "Hello World"

6. 密码强度校验

1
2
3
4
5
6
7
8
9
10
11
function checkPasswordStrength(pwd) {
const rules = [
/.{8,}/, // 至少 8 位
/[a-z]/, // 含小写字母
/[A-Z]/, // 含大写字母
/\d/, // 含数字
/[^a-zA-Z0-9]/, // 含特殊字符
];
const score = rules.filter(r => r.test(pwd)).length;
return score <= 2 ? '弱' : score <= 3 ? '中' : score <= 4 ? '强' : '极强';
}

7. 提取 URL 查询参数

1
2
3
4
5
6
7
8
9
10
function getQueryParams(url) {
const params = {};
url.replace(/(\w+)=([^&#]*)/g, (_, key, val) => {
params[key] = decodeURIComponent(val);
});
return params;
}

getQueryParams('https://example.com?a=1&b=hello&c=%E4%B8%AD');
// { a: "1", b: "hello", c: "中" }

8. 验证字符串是否回文

1
2
3
4
5
6
function isPalindrome(str) {
const clean = str.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
return clean === clean.split('').reverse().join('');
}

isPalindrome('A man, a plan, a canal: Panama'); // true

9. /\d{1,3}(?=(\d{3})+$)/g 的原理

1
2
'12345678'.replace(/\d{1,3}(?=(\d{3})+$)/g, '$&,');
// "12,345,678"
1
2
3
4
5
6
7
8
9
10
11
匹配过程:
\d{1,3} 匹配 1~3 个数字
(?=(\d{3})+$) 后面必须跟 1~n 组正好 3 个数字直到结尾
g 全局匹配

正向匹配(从 12345678 中向右看):
"12345678":
匹配 "12"(因为后面是 "345678" = 两组三位,即 (345)(678) 正好到 $)
匹配 "345"(因为后面是 "678" = 一组三位,到 $)
最后剩下 "678" 不满足条件(后面没有三位数字到结尾)
结果替换为 "12,345,678"

10. 正则匹配陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 陷阱 1:全局 regex 的 lastIndex
const re = /\d+/g;
re.test('abc123'); // true
re.test('abc123'); // false(lastIndex=6,从 index=6 开始匹配)
re.lastIndex = 0; // 手动重置
re.test('abc123'); // true

// 陷阱 2:match 不带 g 与带 g 的结果不同
'abc123def456'.match(/\d+/);
// ["123", index: 3, ...](带捕获组信息)

'abc123def456'.match(/\d+/g);
// ["123", "456"](纯数组,不含捕获组信息)

// 陷阱 3:String.replace 的回调参数
'abcd'.replace(/(\w)(\w)/g, (match, $1, $2, index, input) => {
// match = "ab" / "cd"
// $1 = "a" / "c"
// $2 = "b" / "d"
// index = 0 / 2
return $2 + $1; // 交换两个字符
});
// "badc"