32. 说说你对正则表达式的理解?
问题解析
正则表达式(Regular Expression)是一种用来匹配字符串的强大工具。面试中主要考察正则表达式的语法、常用方法、匹配模式以及实际应用场景。
核心概念
1. 什么是正则表达式
正则表达式是一种描述字符串匹配模式的语言,可以用来检查字符串是否包含某个子串、提取特定格式的子串、替换字符串等。
// 创建正则表达式的两种方式
// 方式1:字面量(推荐)
const regex1 = /abc/;
// 方式2:RegExp 构造函数
const regex2 = new RegExp('abc');
const regex3 = new RegExp('abc', 'gi'); // 带标志
// 使用变量创建
const pattern = 'hello';
const flags = 'i';
const regex4 = new RegExp(pattern, flags);
2. 正则表达式标志(Flags)
| 标志 | 含义 | 说明 |
|---|---|---|
g |
global | 全局匹配,找到所有匹配项 |
i |
ignoreCase | 忽略大小写 |
m |
multiline | 多行匹配,^ 和 $ 匹配每行开头和结尾 |
s |
dotAll | 点号匹配换行符 |
u |
unicode | 正确处理 Unicode |
y |
sticky | 粘性匹配,从 lastIndex 开始匹配 |
const str = 'Hello hello HELLO';
console.log(str.match(/hello/)); // ['hello'] (第一个)
console.log(str.match(/hello/g)); // ['hello', 'hello', 'HELLO']
console.log(str.match(/hello/gi)); // ['Hello', 'hello', 'HELLO']
详细解答
1. 基本元字符
// 1. 字符匹配
/a/ // 匹配字符 'a'
/./ // 匹配除换行符外的任意单个字符
/\d/ // 匹配数字 [0-9]
/\D/ // 匹配非数字 [^0-9]
/\w/ // 匹配单词字符 [a-zA-Z0-9_]
/\W/ // 匹配非单词字符
/\s/ // 匹配空白字符(空格、制表符、换行等)
/\S/ // 匹配非空白字符
// 2. 字符集
/[abc]/ // 匹配 a、b 或 c
/[^abc]/ // 匹配除 a、b、c 外的任意字符
/[a-z]/ // 匹配 a 到 z 的任意小写字母
/[A-Z]/ // 匹配 A 到 Z 的任意大写字母
/[0-9]/ // 匹配任意数字
/[a-zA-Z]/ // 匹配任意字母
// 3. 量词
/a*/ // 匹配 0 个或多个 a
/a+/ // 匹配 1 个或多个 a
/a?/ // 匹配 0 个或 1 个 a
/a{3}/ // 匹配恰好 3 个 a
/a{3,}/ // 匹配至少 3 个 a
/a{3,5}/ // 匹配 3 到 5 个 a
// 4. 边界
/^/ // 匹配字符串开头
/$/ // 匹配字符串结尾
/\b/ // 匹配单词边界
/\B/ // 匹配非单词边界
// 5. 分组和选择
/(abc)/ // 捕获分组
/(?:abc)/ // 非捕获分组
/a|b/ // 匹配 a 或 b
2. 常用方法
const str = 'The quick brown fox jumps over the lazy dog';
const regex = /fox/;
// 1. test() - 测试是否匹配(返回布尔值)
console.log(regex.test(str)); // true
// 2. exec() - 执行匹配(返回匹配结果数组或 null)
const result = regex.exec(str);
console.log(result);
// ['fox', index: 16, input: 'The quick brown fox...', groups: undefined]
// 3. match() - 字符串方法,返回匹配结果
console.log(str.match(/o./g)); // ['ow', 'ox', 'ov', 'er', 'og']
// 4. matchAll() - 返回所有匹配的迭代器
const matches = str.matchAll(/o./g);
for (const match of matches) {
console.log(match[0]); // 'ow', 'ox', 'ov', 'er', 'og'
}
// 5. search() - 返回匹配位置的索引
console.log(str.search(/fox/)); // 16
console.log(str.search(/cat/)); // -1
// 6. replace() - 替换匹配内容
console.log(str.replace(/fox/, 'cat'));
// 'The quick brown cat jumps over the lazy dog'
console.log(str.replace(/the/gi, 'a'));
// 'a quick brown fox jumps over a lazy dog'
// 使用回调函数
console.log(str.replace(/\b\w{4}\b/g, word => word.toUpperCase()));
// 'The QUICK BROWN fox JUMPS OVER the LAZY dog'
// 7. split() - 分割字符串
console.log('a,b,c'.split(/,/)); // ['a', 'b', 'c']
console.log('a, b, c'.split(/,\s*/)); // ['a', 'b', 'c']
3. 捕获组
// 1. 基本捕获组
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const date = '2024-03-15';
const match = date.match(dateRegex);
console.log(match);
// ['2024-03-15', '2024', '03', '15', index: 0, ...]
console.log(match[1]); // '2024' (年)
console.log(match[2]); // '03' (月)
console.log(match[3]); // '15' (日)
// 2. 命名捕获组 (ES2018)
const namedRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const namedMatch = date.match(namedRegex);
console.log(namedMatch.groups);
// { year: '2024', month: '03', day: '15' }
// 3. 非捕获组
const nonCapture = /(?:\d{4})-(\d{2})-(\d{2})/;
const nonCapMatch = date.match(nonCapture);
console.log(nonCapMatch);
// ['2024-03-15', '03', '15', ...] (没有年的单独捕获)
// 4. 反向引用
const repeatRegex = /(\w+)\s+\1/; // 匹配重复的单词
console.log('hello hello'.match(repeatRegex)); // ['hello hello', 'hello']
console.log('world world'.match(repeatRegex)); // ['world world', 'world']
// 命名反向引用
const namedRepeat = /(?<word>\w+)\s+\k<word>/;
console.log('test test'.match(namedRepeat)); // ['test test', 'test']
深入理解
1. 贪婪匹配 vs 惰性匹配
const str = '<div>content1</div><div>content2</div>';
// 贪婪匹配(默认):尽可能匹配更多字符
const greedy = /<div>.*<\/div>/;
console.log(str.match(greedy));
// ['<div>content1</div><div>content2</div>'] (匹配整个字符串)
// 惰性匹配:尽可能匹配更少字符
const lazy = /<div>.*?<\/div>/;
console.log(str.match(lazy));
// ['<div>content1</div>'] (只匹配第一个 div)
// 贪婪量词 vs 惰性量词
// * -> *? (0 次或多次)
// + -> +? (1 次或多次)
// ? -> ?? (0 次或 1 次)
// {n} -> {n}? (恰好 n 次)
// {n,} -> {n,}? (至少 n 次)
// {n,m}-> {n,m}?(n 到 m 次)
2. 前瞻和后顾
// 1. 正向前瞻 (?=...) - 后面跟着什么
const price = 'Price: $100';
console.log(price.match(/\d+(?=\s*USD)/)); // null
console.log(price.match(/\d+(?=\s*USD|\s*$)/)); // ['100']
// 2. 负向前瞻 (?!...) - 后面不跟着什么
console.log('Windows95'.match(/Windows(?!95)/)); // null
console.log('WindowsXP'.match(/Windows(?!95)/)); // ['Windows']
// 3. 正向后顾 (?<=...) - 前面是什么 (ES2018)
const amount = '$100';
console.log(amount.match(/(?<=\$)\d+/)); // ['100']
// 4. 负向后顾 (?<!...) - 前面不是什么 (ES2018)
console.log('USD100'.match(/(?<!\$)\d+/)); // ['100']
console.log('$100'.match(/(?<!\$)\d+/)); // null
// 实用示例:千分位格式化
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
console.log(formatNumber(1234567)); // '1,234,567'
3. 正则表达式性能优化
// ❌ 避免回溯灾难
// 灾难性回溯示例
const badRegex = /(a+)+b/;
// 'aaaaaaaaaaaaaaaaaaaaaaaaaaaa!' 会导致指数级回溯
// ✅ 使用具体字符类替代 .
// 慢
const slow = /.*/;
// 快
const fast = /[^\n]*/;
// ✅ 使用原子组(模拟)
// 避免不必要的回溯
const atomic = /(?>a+)/; // 其他语言支持,JS 需要变通
// ✅ 优先使用字符串方法
// 慢
'hello'.match(/^hello$/);
// 快
'hello' === 'hello';
// ✅ 预编译正则表达式
const regexCache = new Map();
function getRegex(pattern, flags) {
const key = pattern + flags;
if (!regexCache.has(key)) {
regexCache.set(key, new RegExp(pattern, flags));
}
return regexCache.get(key);
}
最佳实践
1. 常用验证模式
// 1. 邮箱验证
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
// 更严格的版本
const strictEmail = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
// 2. 手机号验证(中国大陆)
const phoneRegex = /^1[3-9]\d{9}$/;
// 3. URL 验证
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
// 4. 身份证号(18位)
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
// 5. 密码强度验证(至少8位,包含大小写字母和数字)
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/;
// 6. IPv4 地址验证
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// 7. 中文字符
const chineseRegex = /[\u4e00-\u9fa5]/;
// 验证函数
function validate(regex, value) {
return regex.test(value);
}
2. 字符串处理
// 1. 提取所有链接
const html = '<a href="https://example.com">Link</a><a href="https://test.com">Test</a>';
const links = html.match(/href="([^"]*)"/g)?.map(match => match.match(/href="([^"]*)"/)[1]);
console.log(links); // ['https://example.com', 'https://test.com']
// 2. 驼峰命名转下划线
function camelToSnake(str) {
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
}
console.log(camelToSnake('camelCaseString')); // 'camel_case_string'
// 3. 下划线命名转驼峰
function snakeToCamel(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
console.log(snakeToCamel('snake_case_string')); // 'snakeCaseString'
// 4. 去除 HTML 标签
function stripHtml(html) {
return html.replace(/<[^>]*>/g, '');
}
console.log(stripHtml('<p>Hello <b>World</b></p>')); // 'Hello World'
// 5. 提取查询参数
function parseQueryString(url) {
const params = {};
const queryString = url.split('?')[1];
if (queryString) {
queryString.split('&').forEach(param => {
const [key, value] = param.split('=');
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
});
}
return params;
}
// 或使用 URLSearchParams
function parseQueryStringModern(url) {
return Object.fromEntries(new URLSearchParams(url.split('?')[1]));
}
3. 高级应用
// 1. 模板引擎(简化版)
function template(str, data) {
return str.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return data[key] !== undefined ? data[key] : match;
});
}
const result = template('Hello, {{name}}!', { name: 'Alice' });
console.log(result); // 'Hello, Alice!'
// 2. Markdown 链接提取
const markdown = '[Google](https://google.com) [Baidu](https://baidu.com)';
const mdLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
let match;
while ((match = mdLinkRegex.exec(markdown)) !== null) {
console.log(`Text: ${match[1]}, URL: ${match[2]}`);
}
// 3. 日志解析
const logLine = '2024-03-15 10:30:45 [INFO] User login successful: user123';
const logRegex = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] (.+)$/;
const logMatch = logLine.match(logRegex);
if (logMatch) {
const [, timestamp, level, message] = logMatch;
console.log({ timestamp, level, message });
}
// 4. 代码高亮(简化版)
function highlightCode(code) {
// 高亮字符串
code = code.replace(/(['"`])(.*?)\1/g, '<span class="string">$&</span>');
// 高亮关键字
code = code.replace(/\b(const|let|var|function|return|if|else)\b/g, '<span class="keyword">$1</span>');
// 高亮数字
code = code.replace(/\b\d+\b/g, '<span class="number">$&</span>');
return code;
}
面试要点
- 元字符:掌握
.\d\w\s^$*+?{}[]()|等 - 贪婪 vs 惰性:
*vs*?,+vs+? - 捕获组:
()捕获,(?:)非捕获,(?<name>)命名捕获 - 前瞻后顾:
(?=)(?! )(?<=)(?<!) - 常用方法:
test()exec()match()replace()search()split()
常见面试题
// 面试题 1:写一个正则匹配邮箱
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
// 面试题 2:提取 URL 中的域名
function extractDomain(url) {
const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
return match ? match[1] : null;
}
console.log(extractDomain('https://www.example.com/path')); // 'example.com'
// 面试题 3:实现千分位格式化
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// 面试题 4:验证密码强度(至少8位,包含大小写字母、数字、特殊字符)
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
// 面试题 5:去除字符串前后空格(不使用 trim)
function trim(str) {
return str.replace(/^\s+|\s+$/g, '');
}
// 面试题 6:统计字符串中出现次数最多的字符
function maxChar(str) {
const charMap = {};
let max = 0;
let maxChar = '';
str.replace(/\S/g, char => {
charMap[char] = (charMap[char] || 0) + 1;
if (charMap[char] > max) {
max = charMap[char];
maxChar = char;
}
});
return maxChar;
}
console.log(maxChar('hello world')); // 'l'
// 面试题 7:实现一个简单的模板引擎
function template(str, data) {
return str.replace(/\$\{(\w+)\}/g, (match, key) => {
return data[key] !== undefined ? data[key] : match;
});
}
console.log(template('Name: ${name}, Age: ${age}', { name: 'Alice', age: 25 }));
// 'Name: Alice, Age: 25'
// 面试题 8:正则表达式回溯问题
// 避免使用 (a+)+ 这种会导致灾难性回溯的模式
// 如果必须使用,考虑限制长度或使用原子组(在其他语言中)
// 面试题 9:为什么正则表达式中 . 不能匹配换行符?如何让它匹配?
// 默认情况下 . 不匹配 \n \r \u2028 \u2029
// 使用 [\s\S] 或 [^] 或添加 s 标志(ES2018)
const multiline = 'line1\nline2';
console.log(multiline.match(/[\s\S]+/)[0]); // 'line1\nline2'
console.log(multiline.match(/.+/s)[0]); // 'line1\nline2' (使用 dotAll 标志)