返回首页

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;
}

面试要点

  1. 元字符:掌握 . \d \w \s ^ $ * + ? {} [] () |
  2. 贪婪 vs 惰性* vs *?+ vs +?
  3. 捕获组() 捕获,(?:) 非捕获,(?<name>) 命名捕获
  4. 前瞻后顾(?=) (?! ) (?<=) (?<!)
  5. 常用方法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 标志)