正则表达式是处理字符串的强大工具。本文详解 JavaScript 正则表达式的语法和实战技巧。
基础语法
创建正则表达式
// 字面量方式
const regex1 = /pattern/flags;
// 构造函数方式
const regex2 = new RegExp('pattern', 'flags');
// 示例
const emailRegex = /^\w+@\w+\.\w+$/;
const dynamicRegex = new RegExp(`user_${userId}`, 'i');
常用标志
// g - 全局匹配
'hello hello'.match(/hello/g); // ['hello', 'hello']
// i - 忽略大小写
/hello/i.test('HELLO'); // true
// m - 多行模式
/^start/m.test('line1\nstart'); // true
// s - 点号匹配换行
/a.b/s.test('a\nb'); // true
// u - Unicode 模式
/\u{1F600}/u.test('😀'); // true
// y - 粘性匹配
const sticky = /foo/y;
sticky.lastIndex = 3;
sticky.test('xxxfoo'); // true
// 组合使用
const regex = /pattern/gim;
元字符
// . - 匹配任意单个字符(除换行符)
/a.c/.test('abc'); // true
/a.c/.test('a\nc'); // false
// ^ - 匹配开头
/^hello/.test('hello world'); // true
// $ - 匹配结尾
/world$/.test('hello world'); // true
// | - 或运算
/cat|dog/.test('I have a cat'); // true
// \ - 转义特殊字符
/\$100/.test('$100'); // true
/1\+1/.test('1+1'); // true
字符类
// [abc] - 匹配方括号内任意字符
/[aeiou]/.test('hello'); // true
// [^abc] - 匹配非方括号内的字符
/[^0-9]/.test('abc'); // true
// [a-z] - 范围匹配
/[a-zA-Z]/.test('Hello'); // true
// 预定义字符类
/\d/.test('123'); // true - 数字 [0-9]
/\D/.test('abc'); // true - 非数字 [^0-9]
/\w/.test('hello'); // true - 单词字符 [a-zA-Z0-9_]
/\W/.test('@#$'); // true - 非单词字符
/\s/.test(' \t\n'); // true - 空白字符
/\S/.test('abc'); // true - 非空白字符
// 边界匹配
/\bword\b/.test('a word here'); // true - 单词边界
/\Bword/.test('sword'); // true - 非单词边界
量词
// * - 零次或多次
/ab*c/.test('ac'); // true
/ab*c/.test('abbc'); // true
// + - 一次或多次
/ab+c/.test('ac'); // false
/ab+c/.test('abc'); // true
// ? - 零次或一次
/colou?r/.test('color'); // true
/colou?r/.test('colour'); // true
// {n} - 恰好 n 次
/a{3}/.test('aaa'); // true
/a{3}/.test('aa'); // false
// {n,} - 至少 n 次
/a{2,}/.test('aaa'); // true
// {n,m} - n 到 m 次
/a{2,4}/.test('aaa'); // true
// 贪婪 vs 非贪婪
'<div>content</div>'.match(/<.*>/); // ['<div>content</div>']
'<div>content</div>'.match(/<.*?>/); // ['<div>']
分组与捕获
捕获组
// 基本捕获组
const match = /(\d{4})-(\d{2})-(\d{2})/.exec('2025-01-28');
console.log(match[0]); // '2025-01-28' - 完整匹配
console.log(match[1]); // '2025' - 第一个捕获组
console.log(match[2]); // '01' - 第二个捕获组
console.log(match[3]); // '28' - 第三个捕获组
// 使用 match
const result = 'hello world'.match(/(\w+) (\w+)/);
console.log(result[1], result[2]); // 'hello' 'world'
// 嵌套捕获组
const nested = /((a)(b))/.exec('ab');
console.log(nested[1]); // 'ab'
console.log(nested[2]); // 'a'
console.log(nested[3]); // 'b'
命名捕获组
// ES2018 命名捕获组
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = dateRegex.exec('2025-01-28');
console.log(match.groups.year); // '2025'
console.log(match.groups.month); // '01'
console.log(match.groups.day); // '28'
// 解构赋值
const { groups: { year, month, day } } = dateRegex.exec('2025-01-28');
// 在替换中使用
'2025-01-28'.replace(dateRegex, '$<month>/$<day>/$<year>');
// '01/28/2025'
非捕获组
// (?:pattern) - 分组但不捕获
const regex = /(?:https?|ftp):\/\/(\w+)/;
const match = regex.exec('https://example');
console.log(match[0]); // 'https://example'
console.log(match[1]); // 'example' - 只有一个捕获组
反向引用
// \n 引用第 n 个捕获组
const duplicateWords = /(\w+)\s+\1/;
duplicateWords.test('hello hello'); // true
duplicateWords.test('hello world'); // false
// 命名反向引用
const repeat = /(?<word>\w+)\s+\k<word>/;
repeat.test('the the'); // true
// 匹配引号内容(开闭引号相同)
const quoted = /(['"]).*?\1/;
quoted.test('"hello"'); // true
quoted.test("'hello'"); // true
quoted.test('"hello\''); // false
断言
先行断言
// 正向先行断言 (?=pattern)
// 匹配后面跟着 pattern 的位置
/\d+(?=元)/.exec('100元'); // ['100']
/\d+(?=元)/.exec('100美元'); // null
// 负向先行断言 (?!pattern)
// 匹配后面不跟着 pattern 的位置
/\d+(?!元)/.exec('100美元'); // ['100']
/\d+(?!元)/.exec('100元'); // ['10'] - 匹配到不在"元"前的数字
后行断言
// 正向后行断言 (?<=pattern) - ES2018
// 匹配前面是 pattern 的位置
/(?<=\$)\d+/.exec('$100'); // ['100']
/(?<=\$)\d+/.exec('¥100'); // null
// 负向后行断言 (?<!pattern)
// 匹配前面不是 pattern 的位置
/(?<!\$)\d+/.exec('¥100'); // ['100']
实用示例
// 密码强度检查(必须包含大小写和数字)
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
strongPassword.test('Password123'); // true
strongPassword.test('password123'); // false
// 提取不在引号内的数字
const notInQuotes = /(?<!['"]\d*)\d+(?!\d*['"])/g;
// 千分位格式化
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
formatNumber(1234567); // '1,234,567'
常用方法
RegExp 方法
const regex = /hello/g;
// test - 测试是否匹配
regex.test('hello world'); // true
// exec - 执行匹配
let match;
const str = 'hello hello';
regex.lastIndex = 0; // 重置索引
while ((match = regex.exec(str)) !== null) {
console.log(`Found ${match[0]} at ${match.index}`);
}
// Found hello at 0
// Found hello at 6
String 方法
const str = 'hello world hello';
// match - 返回匹配结果
str.match(/hello/g); // ['hello', 'hello']
str.match(/hello/); // ['hello', index: 0, ...]
// matchAll - 返回迭代器(ES2020)
for (const match of str.matchAll(/hello/g)) {
console.log(match.index, match[0]);
}
// search - 返回首次匹配索引
str.search(/world/); // 6
// replace - 替换
str.replace(/hello/g, 'hi'); // 'hi world hi'
// replaceAll - 替换所有(ES2021)
str.replaceAll('hello', 'hi'); // 'hi world hi'
// split - 分割
'a,b;c|d'.split(/[,;|]/); // ['a', 'b', 'c', 'd']
替换回调
// 使用函数作为替换参数
const result = 'hello world'.replace(/(\w+)/g, (match, p1, offset) => {
return p1.toUpperCase();
});
// 'HELLO WORLD'
// 模板字符串转换
const template = 'Hello, {{name}}! Today is {{day}}.';
const data = { name: 'Alice', day: 'Monday' };
const output = template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return data[key] || match;
});
// 'Hello, Alice! Today is Monday.'
实战应用
表单验证
const validators = {
// 邮箱
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
// 手机号(中国)
phone: /^1[3-9]\d{9}$/,
// 身份证
idCard: /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/,
// URL
url: /^https?:\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?$/,
// IP 地址
ip: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
// 强密码
strongPassword: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
// 用户名
username: /^[a-zA-Z][a-zA-Z0-9_]{2,15}$/,
// 中文
chinese: /^[\u4e00-\u9fa5]+$/
};
function validate(type, value) {
return validators[type]?.test(value) ?? false;
}
validate('email', 'test@example.com'); // true
validate('phone', '13812345678'); // true
文本处理
// 提取所有链接
function extractLinks(html) {
const regex = /href=["']([^"']+)["']/g;
const links = [];
let match;
while ((match = regex.exec(html)) !== null) {
links.push(match[1]);
}
return links;
}
// 移除 HTML 标签
function stripHtml(html) {
return html.replace(/<[^>]*>/g, '');
}
// 驼峰转换
function camelToKebab(str) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
camelToKebab('backgroundColor'); // 'background-color'
function kebabToCamel(str) {
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
}
kebabToCamel('background-color'); // 'backgroundColor'
// 首字母大写
function capitalize(str) {
return str.replace(/\b\w/g, c => c.toUpperCase());
}
capitalize('hello world'); // 'Hello World'
// 压缩空白
function compressWhitespace(str) {
return str.replace(/\s+/g, ' ').trim();
}
数据提取
// 解析 URL 参数
function parseQuery(url) {
const params = {};
const regex = /[?&]([^=&]+)=([^&]*)/g;
let match;
while ((match = regex.exec(url)) !== null) {
params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
}
return params;
}
parseQuery('https://example.com?name=test&id=123');
// { name: 'test', id: '123' }
// 解析 CSV
function parseCSV(csv) {
const regex = /(?:^|,)(?:"([^"]*(?:""[^"]*)*)"|([^,]*))/g;
const rows = csv.split('\n');
return rows.map(row => {
const values = [];
let match;
while ((match = regex.exec(row)) !== null) {
values.push(match[1]?.replace(/""/g, '"') ?? match[2] ?? '');
}
return values;
});
}
// 日志解析
function parseLog(log) {
const regex = /\[(?<timestamp>[^\]]+)\]\s+(?<level>\w+)\s+(?<message>.*)/;
const match = regex.exec(log);
return match?.groups ?? null;
}
parseLog('[2025-01-28 10:30:00] ERROR Connection failed');
// { timestamp: '2025-01-28 10:30:00', level: 'ERROR', message: 'Connection failed' }
语法高亮
function highlightCode(code) {
const rules = [
// 关键字
{ pattern: /\b(const|let|var|function|return|if|else|for|while)\b/g, class: 'keyword' },
// 字符串
{ pattern: /(["'`])(?:(?!\1)[^\\]|\\.)*\1/g, class: 'string' },
// 数字
{ pattern: /\b\d+\.?\d*\b/g, class: 'number' },
// 注释
{ pattern: /\/\/.*$/gm, class: 'comment' },
{ pattern: /\/\*[\s\S]*?\*\//g, class: 'comment' }
];
let result = code;
rules.forEach(({ pattern, class: className }) => {
result = result.replace(pattern, `<span class="${className}">$&</span>`);
});
return result;
}
性能优化
避免回溯陷阱
// 危险模式 - 可能导致灾难性回溯
const dangerous = /a+a+b/;
// 优化版本
const optimized = /a{2,}b/;
// 使用原子组(通过先行断言模拟)
// 匹配不回溯
const atomic = /(?=(\d+))\1/;
// 使用占有量词的替代方案
// 原始:/\d++/ (其他语言支持)
// JS 替代:通过逻辑控制避免回溯
正则表达式优化技巧
// 1. 预编译正则
const emailRegex = /^[\w.+-]+@[\w-]+\.[\w.-]+$/;
function validateEmail(email) {
return emailRegex.test(email); // 复用编译好的正则
}
// 2. 具体化模式
// 差:/.*?pattern/
// 好:/[^p]*pattern/ 或 /[\s\S]*?pattern/
// 3. 使用非捕获组
// 差:/(foo|bar|baz)/
// 好:/(?:foo|bar|baz)/
// 4. 锚定模式
// 差:/pattern/
// 好:/^pattern$/ 或 /\bpattern\b/
// 5. 提取公共前缀
// 差:/javascript|javafx|java/
// 好:/java(?:script|fx)?/
最佳实践总结
正则表达式最佳实践:
┌─────────────────────────────────────────────────────┐
│ │
│ 可读性 │
│ ├── 使用命名捕获组 │
│ ├── 添加注释说明 │
│ ├── 拆分复杂正则 │
│ └── 使用 RegExp 构造函数组合 │
│ │
│ 性能 │
│ ├── 预编译正则表达式 │
│ ├── 避免回溯陷阱 │
│ ├── 使用具体字符类 │
│ └── 锚定匹配位置 │
│ │
│ 安全 │
│ ├── 验证用户输入的正则 │
│ ├── 设置匹配超时 │
│ ├── 限制正则复杂度 │
│ └── 避免 ReDoS 攻击 │
│ │
└─────────────────────────────────────────────────────┘
| 场景 | 推荐做法 |
|---|---|
| 简单匹配 | 字符串方法 includes/indexOf |
| 复杂模式 | 正则表达式 |
| 动态模式 | RegExp 构造函数 |
| 多次使用 | 预编译存储 |
掌握正则表达式,解锁强大的文本处理能力。