第 34 章 正则表达式

第 34 章 正则表达式

正则表达式是 JavaScript 里的"字符串匹配神器"。学会了它,你就能在字符串里"大海捞针",甚至"以一敌百"。

34.1 正则基础

创建正则:字面量 / new RegExp()

1
2
3
4
5
6
7
8
9
// 字面量方式(推荐)
const regex1 = /hello/;

// 构造函数方式(可以动态创建)
const regex2 = new RegExp('hello');

// 两者等价
console.log(regex1.test('hello world')); // 打印结果: true
console.log(regex2.test('hello world')); // 打印结果: true

test() / exec()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// test():返回布尔值,简单快速
const regex = /hello/;
console.log(regex.test('hello world')); // 打印结果: true
console.log(regex.test('goodbye')); // 打印结果: false

// exec():返回匹配结果或 null,更详细
const result = regex.exec('hello world');
if (result) {
    console.log('匹配到:' + result[0]); // 打印结果: 匹配到:hello
    console.log('索引:' + result.index); // 打印结果: 索引:0
}

下一节,我们来学习字符与元字符!

34.2 字符与元字符

字符类:[abc] / [0-9] / [^abc] / \d \D \w \W \s \S / .

 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
28
29
// [abc]:匹配 a、b 或 c
console.log(/[aeiou]/.test('hello')); // 打印结果: true

// [0-9]:匹配数字
console.log(/[0-9]/.test('hello')); // 打印结果: false

// [^abc]:匹配除了 a、b、c 以外的字符
console.log(/[^aeiou]/.test('hello')); // 打印结果: true

// \d:匹配数字,等价于 [0-9]
console.log(/\d/.test('abc123')); // 打印结果: true

// \D:匹配非数字,等价于 [^0-9]
console.log(/\D/.test('123')); // 打印结果: false

// \w:匹配单词字符,等价于 [a-zA-Z0-9_]
console.log(/\w/.test('_')); // 打印结果: true

// \W:匹配非单词字符
console.log(/\W/.test('!')); // 打印结果: true

// \s:匹配空白字符(空格、制表符、换行符等)
console.log(/\s/.test('hello world')); // 打印结果: true

// \S:匹配非空白字符
console.log(/\S/.test('   ')); // 打印结果: false

// .:匹配除换行符以外的任意字符
console.log(/./.test('\n')); // 打印结果: false

量词:{n} / {n,} / {n,m} / * / + / ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// {n}:正好匹配 n 次
console.log(/a{3}/.test('aa')); // 打印结果: false
console.log(/a{3}/.test('aaa')); // 打印结果: true

// {n,}:至少匹配 n 次
console.log(/a{2,}/.test('a')); // 打印结果: false
console.log(/a{2,}/.test('aaa')); // 打印结果: true

// {n,m}:匹配 n 到 m 次
console.log(/a{2,4}/.test('aaa')); // 打印结果: true

// *:匹配 0 次或多次,等价于 {0,}
console.log(/ab*c/.test('ac')); // 打印结果: true
console.log(/ab*c/.test('abc')); // 打印结果: true

// +:匹配 1 次或多次,等价于 {1,}
console.log(/ab+c/.test('ac')); // 打印结果: false
console.log(/ab+c/.test('abc')); // 打印结果: true

// ?:匹配 0 次或 1 次,等价于 {0,1}
console.log(/colou?r/.test('color')); // 打印结果: true
console.log(/colou?r/.test('colour')); // 打印结果: true

贪婪 vs 非贪婪:.*? / +? / ??

贪婪匹配会尽可能多地匹配,非贪婪匹配会尽可能少地匹配。

1
2
3
4
5
// 贪婪匹配:.* 会尽可能多地匹配
console.log('"hello" and "world"'.match(/".*"/)[0]); // 打印结果: "hello" and "world"

// 非贪婪匹配:.*? 会尽可能少地匹配
console.log('"hello" and "world"'.match(/".*?"/)[0]); // 打印结果: "hello"

边界:^ / $ / \b / \B

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ^:匹配字符串开头
console.log(/^hello/.test('hello world')); // 打印结果: true
console.log(/^hello/.test('hi hello')); // 打印结果: false

// $:匹配字符串结尾
console.log(/world$/.test('hello world')); // 打印结果: true
console.log(/world$/.test('world hi')); // 打印结果: false

// \b:匹配单词边界
console.log(/\bword\b/.test('a word here')); // 打印结果: true
console.log(/\bword\b/.test('sword here')); // 打印结果: false

// \B:匹配非单词边界
console.log(/\Bword/.test('sword')); // 打印结果: true

下一节,我们来学习分组与引用!

34.3 分组与引用

捕获分组:(abc)

1
2
3
4
5
6
// 捕获分组:用圆括号包裹,会被捕获
const result = /(\d{4})-(\d{2})-(\d{2})/.exec('2024-03-24');
console.log(result[0]); // 打印结果: 2024-03-24
console.log(result[1]); // 打印结果: 2024
console.log(result[2]); // 打印结果: 03
console.log(result[3]); // 打印结果: 24

非捕获分组:(?:abc)

1
2
3
4
5
// 非捕获分组:用 (?:),不会捕获
const result = /(?:\d{4})-(\d{2})-(\d{2})/.exec('2024-03-24');
console.log(result[0]); // 打印结果: 2024-03-24
console.log(result[1]); // 打印结果: 03(只捕获第二个和第三个)
console.log(result[2]); // 打印结果: 24

反向引用:\1 \2

1
2
3
4
5
6
// 反向引用:\1 引用第一个分组,\2 引用第二个分组
console.log(/(\w)\1/.test('aa')); // 打印结果: true(两个相同的字母)
console.log(/(\w)\1/.test('ab')); // 打印结果: false

// 应用:匹配重复的单词
console.log(/\b(\w+)\s\1\b/.test('the the')); // 打印结果: true

或运算:|

1
2
3
4
5
6
7
// 或运算:|
console.log(/cat|dog/.test('I have a cat')); // 打印结果: true
console.log(/cat|dog/.test('I have a dog')); // 打印结果: true

// 分组中使用或
console.log(/(foo|bar)baz/.test('foobaz')); // 打印结果: true
console.log(/(foo|bar)baz/.test('barbaz')); // 打印结果: true

下一节,我们来学习字符串方法!

34.4 字符串方法

match / search / replace / split + 正则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const str = 'Hello World! 123';

// match:返回所有匹配
console.log(str.match(/\d+/g)); // 打印结果: ['123']

// search:返回第一个匹配的索引
console.log(str.search(/\d+/)); // 打印结果: 14

// replace:替换匹配
console.log(str.replace(/\d+/, '456')); // 打印结果: Hello World! 456

// split:分割字符串
console.log('a,b,c'.split(/,/)); // 打印结果: ['a', 'b', 'c']

replace 回调函数与反向引用:$1 $2 $` $’ $&

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// $1, $2:引用分组
console.log('2024-03-24'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3/$2/$1'));
// 打印结果: 24/03/2024

// $&:整个匹配
console.log('hello'.replace(/\w+/, '[$&]')); // 打印结果: [hello]

// $`:匹配前面的部分
console.log('hello world'.replace(/world/, '$`hello '));
// 打印结果: hello hello world

// $':匹配后面的部分
console.log('hello world'.replace(/hello/, "$' world"));
// 打印结果: hello world world

// 回调函数
console.log('2024-03-24'.replace(/(\d{4})-(\d{2})-(\d{2})/, function(match, y, m, d) {
    return m + '/' + d + '/' + y;
}));
// 打印结果: 03/24/2024

下一节,我们来学习常用场景!

34.5 常用场景

手机号验证

1
2
3
4
5
// 中国大陆手机号:1开头,11位数字
const phoneRegex = /^1[3-9]\d{9}$/;
console.log(phoneRegex.test('13812345678')); // 打印结果: true
console.log(phoneRegex.test('12812345678')); // 打印结果: false(1开头,但2不符合)
console.log(phoneRegex.test('1381234567')); // 打印结果: false(只有10位)

邮箱验证

1
2
3
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
console.log(emailRegex.test('test@example.com')); // 打印结果: true
console.log(emailRegex.test('invalid@email')); // 打印结果: false

URL 参数解析

1
2
3
4
5
6
7
8
9
const url = 'https://example.com/search?q=javascript&page=1';

const params = {};
url.match(/[?&]([^&=]+)=([^&]*)/g).forEach(function(match) {
    const [, key, value] = match.split('=');
    params[decodeURIComponent(key)] = decodeURIComponent(value);
});

console.log(params); // 打印结果: { q: 'javascript', page: '1' }

敏感词替换

1
2
3
4
5
6
7
8
const sensitiveWords = ['暴力', '色情', '赌博'];

function filterSensitive(text) {
    const regex = new RegExp(sensitiveWords.join('|'), 'g');
    return text.replace(regex, '***');
}

console.log(filterSensitive('这是一段包含暴力的文字')); // 打印结果: 这是一段包含***的文字

千分位格式化

1
2
3
4
5
6
7
// 数字千分位格式化
function formatNumber(num) {
    return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

console.log(formatNumber(1234567)); // 打印结果: 1,234,567
console.log(formatNumber(1234567.89)); // 打印结果: 1,234,567.89

密码强度验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function validatePassword(password) {
    const errors = [];
    
    if (password.length < 8) errors.push('密码至少8位');
    if (!/[a-z]/.test(password)) errors.push('需要包含小写字母');
    if (!/[A-Z]/.test(password)) errors.push('需要包含大写字母');
    if (!/\d/.test(password)) errors.push('需要包含数字');
    if (!/[!@#$%^&*]/.test(password)) errors.push('需要包含特殊字符');
    
    return {
        valid: errors.length === 0,
        errors: errors
    };
}

console.log(validatePassword('Abc123!')); // 打印结果: { valid: true, errors: [] }
console.log(validatePassword('weak')); // 打印结果: { valid: false, errors: [...] }

trim 的正则实现

1
2
3
4
5
6
// 去除首尾空白
function trim(str) {
    return str.replace(/^\s+|\s+$/g, '');
}

console.log(trim('  hello world  ')); // 打印结果: hello world

本章小结

本章我们学习了正则表达式:

  1. 正则基础:创建正则(字面量/RegExp)、test/exec 方法。
  2. 字符与元字符:字符类、量词、贪婪/非贪婪、边界。
  3. 分组与引用:捕获分组、非捕获分组、反向引用、或运算。
  4. 字符串方法:match、search、replace、split 与正则的结合。
  5. 常用场景:手机号、邮箱、URL参数、千分位、密码强度等验证。

正则表达式是 JavaScript 开发中的"瑞士军刀",掌握它能让你的字符串处理能力大幅提升。

下一章,我们要学习错误处理——让程序优雅地"容错"!