返回首页

06-正则表达式详解

分类:01-JavaScript基础核心
发布于:
阅读时间:92 分钟

正则表达式详解

📋 学习目标

  • 掌握正则表达式的基本语法和元字符
  • 学会创建和使用正则表达式
  • 理解正则表达式的匹配模式和标志
  • 掌握常见正则表达式模式和应用场景

🎯 正则表达式基础

1. 什么是正则表达式?

正则表达式(Regular Expression,简称RegExp)是一种用于匹配字符串模式的强大工具。它使用特殊的字符序列来描述和匹配字符串的规则。

// 基本概念
const pattern = /hello/;     // 字面量方式创建
const pattern2 = new RegExp('hello'); // 构造函数方式创建

const text = 'hello world';
console.log(pattern.test(text)); // true

2. 创建正则表达式

// 方法1:字面量(推荐)
const pattern1 = /ab+c/;

// 方法2:构造函数(动态模式)
const pattern2 = new RegExp('ab+c');

// 方法3:构造函数(从字符串创建正则)
const str = 'ab+c';
const pattern3 = new RegExp(str);

// 方法4:使用模板变量
const word = 'hello';
const pattern4 = new RegExp(`\\b${word}\\b`); // \bhello\b

3. 正则表达式的方法

const pattern = /hello/g;
const text = 'hello world, hello everyone';

// test() - 测试是否匹配
console.log(pattern.test(text)); // true

// exec() - 执行匹配并返回详细信息
let match = pattern.exec(text);
console.log(match);
// ['hello', index: 0, input: 'hello world, hello everyone', groups: undefined]

match = pattern.exec(text);
console.log(match);
// ['hello', index: 13, input: 'hello world, hello everyone', groups: undefined]

match = pattern.exec(text);
console.log(match); // null (没有更多匹配)

// 重置lastIndex
pattern.lastIndex = 0;

🔧 字符类和元字符

1. 基本字符类

// \d - 数字字符 (0-9)
const digitPattern = /\d/;
console.log(digitPattern.test('123')); // true
console.log(digitPattern.test('abc')); // false

// \D - 非数字字符
const nonDigitPattern = /\D/;
console.log(nonDigitPattern.test('123')); // false
console.log(nonDigitPattern.test('abc')); // true

// \w - 单词字符 (字母、数字、下划线)
const wordPattern = /\w/;
console.log(wordPattern.test('abc123')); // true
console.log(wordPattern.test('hello_world')); // true
console.log(wordPattern.test('hello-world')); // false

// \W - 非单词字符
const nonWordPattern = /\W/;
console.log(nonWordPattern.test('hello-world')); // true
console.log(nonWordPattern.test('hello_world')); // false

// \s - 空白字符 (空格、制表符、换行符等)
const spacePattern = /\s/;
console.log(spacePattern.test('hello world')); // true
console.log(spacePattern.test('helloworld')); // false

// \S - 非空白字符
const nonSpacePattern = /\S/;
console.log(nonSpacePattern.test('hello world')); // true (h是非空白)
console.log(nonSpacePattern.test('   ')); // false

// . - 任意字符(除换行符外)
const anyPattern = /./;
console.log(anyPattern.test('a')); // true
console.log(anyPattern.test('1')); // true
console.log(anyPattern.test('!')); // true
console.log(anyPattern.test('\n')); // false (默认情况下)

2. 自定义字符类

// [abc] - 匹配a、b或c中的任意一个
const abcPattern = /[abc]/;
console.log(abcPattern.test('apple')); // true (包含a)
console.log(abcPattern.test('banana')); // true (包含a)
console.log(abcPattern.test('xyz')); // false

// [a-z] - 匹配小写字母
const lowercasePattern = /[a-z]/;
console.log(lowercasePattern.test('Hello')); // false
console.log(lowercasePattern.test('hello')); // true

// [A-Z] - 匹配大写字母
const uppercasePattern = /[A-Z]/;
console.log(uppercasePattern.test('Hello')); // true
console.log(uppercasePattern.test('hello')); // false

// [0-9] - 匹配数字
const numberPattern = /[0-9]/;
console.log(numberPattern.test('abc123')); // true
console.log(numberPattern.test('abc')); // false

// [a-zA-Z0-9] - 匹配字母和数字
const alnumPattern = /[a-zA-Z0-9]/;
console.log(alnumPattern.test('abc123')); // true
console.log(alnumPattern.test('!@#')); // false

// [^abc] - 匹配除a、b、c外的任意字符
const notAbcPattern = /[^abc]/;
console.log(notAbcPattern.test('apple')); // true (包含p、l、e)
console.log(notAbcPattern.test('abc')); // false

3. 预定义字符类

// Unicode属性类(ES2018+)
const letterPattern = /\p{Letter}/u;     // 任何语言的字母
const numberPattern = /\p{Number}/u;     // 任何语言的数字
const chinesePattern = /\p{Script=Han}/u; // 汉字

console.log(chinesePattern.test('你好')); // true
console.log(chinesePattern.test('hello')); // false

// 更多Unicode属性
const emojiPattern = /\p{Emoji}/u;       // 表情符号
const currencyPattern = /\p{Currency_Symbol}/u; // 货币符号

console.log(emojiPattern.test('😀')); // true
console.log(currencyPattern.test('$')); // true

📊 量词

1. 基本量词

// * - 0次或多次
const starPattern = /ab*/;
console.log(starPattern.test('a'));     // true (b出现0次)
console.log(starPattern.test('ab'));    // true (b出现1次)
console.log(starPattern.test('abbb'));  // true (b出现3次)

// + - 1次或多次
const plusPattern = /ab+/;
console.log(plusPattern.test('a'));     // false (b需要至少1次)
console.log(plusPattern.test('ab'));    // true (b出现1次)
console.log(plusPattern.test('abbb'));  // true (b出现3次)

// ? - 0次或1次
const questionPattern = /ab?/;
console.log(questionPattern.test('a'));     // true (b出现0次)
console.log(questionPattern.test('ab'));    // true (b出现1次)
console.log(questionPattern.test('abb'));   // false (b出现2次)

2. 精确量词

// {n} - 恰好n次
const exactPattern = /ab{3}/;
console.log(exactPattern.test('ab'));     // false
console.log(exactPattern.test('abbb'));   // true
console.log(exactPattern.test('abbbb'));  // false

// {n,} - 至少n次
const atLeastPattern = /ab{2,}/;
console.log(atLeastPattern.test('ab'));     // false
console.log(atLeastPattern.test('abb'));    // true
console.log(atLeastPattern.test('abbbb'));  // true

// {n,m} - n到m次
const rangePattern = /ab{2,4}/;
console.log(rangePattern.test('ab'));      // false
console.log(rangePattern.test('abb'));     // true
console.log(rangePattern.test('abbb'));    // true
console.log(rangePattern.test('abbbb'));   // true
console.log(rangePattern.test('abbbbb'));  // false (超过4次)

3. 贪婪与惰性匹配

// 贪婪匹配(默认)- 尽可能多地匹配
const greedyPattern = /<.*>/;
const text = '<div>content</div>';
console.log(text.match(greedyPattern)); // ['<div>content</div>']

// 惰性匹配 - 尽可能少地匹配
const lazyPattern = /<.*?>/;
console.log(text.match(lazyPattern)); // ['<div>']

// 实际应用:提取HTML标签内容
const htmlText = '<p>First paragraph</p><p>Second paragraph</p>';

// 贪婪匹配(可能不是期望的结果)
const greedyMatch = htmlText.match(/<p>.*<\/p>/);
console.log(greedyMatch); // ['<p>First paragraph</p><p>Second paragraph</p>']

// 惰性匹配(期望的结果)
const lazyMatch = htmlText.match(/<p>.*?<\/p>/g);
console.log(lazyMatch); // ['<p>First paragraph</p>', '<p>Second paragraph</p>']

🎯 锚点和边界

1. 位置锚点

// ^ - 字符串开始
const startPattern = /^Hello/;
console.log(startPattern.test('Hello world')); // true
console.log(startPattern.test('Say Hello')); // false

// $ - 字符串结束
const endPattern = /world$/;
console.log(endPattern.test('Hello world')); // true
console.log(endPattern.test('world peace')); // false

// 组合使用:精确匹配
const exactPattern = /^Hello world$/;
console.log(exactPattern.test('Hello world')); // true
console.log(exactPattern.test('Hello world!')); // false

2. 单词边界

// \b - 单词边界
const wordBoundaryPattern = /\bcat\b/;
console.log(wordBoundaryPattern.test('The cat sat')); // true
console.log(wordBoundaryPattern.test('concatenate')); // false

// 单词边界的位置示例
const text = 'Hello world! 123';
const boundaries = text.match(/\b/g);
console.log(boundaries); // 在每个单词边界处匹配

// \B - 非单词边界
const nonWordBoundaryPattern = /\Bcat\B/;
console.log(nonWordBoundaryPattern.test('concatenate')); // true
console.log(nonWordBoundaryPattern.test('The cat sat')); // false

3. 多行模式

// 多行模式下的^和$
const multilineText = `First line
Second line
Third line`;

// 普通模式
const normalPattern = /^Second/;
console.log(normalPattern.test(multilineText)); // false

// 多行模式
const multilinePattern = /^Second/m;
console.log(multilinePattern.test(multilineText)); // true

// 匹配每行的开始
const lineStartPattern = /^/gm;
const lines = multilineText.match(lineStartPattern);
console.log(lines); // ['', '', '', ''] (匹配每行的开始)

🔄 分组和捕获

1. 基本分组

// () - 分组
const groupPattern = /(ab)+/;
console.log(groupPattern.test('ab'));      // true
console.log(groupPattern.test('abab'));    // true
console.log(groupPattern.test('aba'));     // false

// 捕获组内容
const capturePattern = /(\d{4})-(\d{2})-(\d{2})/;
const date = '2023-12-25';
const match = date.match(capturePattern);
console.log(match);
// [
//   '2023-12-25',    // 完整匹配
//   '2023',          // 第1个捕获组
//   '12',            // 第2个捕获组
//   '25',            // 第3个捕获组
//   index: 0,
//   input: '2023-12-25',
//   groups: undefined
// ]

// 访问捕获组
console.log(match[1]); // '2023' (年份)
console.log(match[2]); // '12'   (月份)
console.log(match[3]); // '25'   (日期)

2. 非捕获组

// (?:...) - 非捕获组
const nonCapturePattern = /(?:ab)+/;
const match = nonCapturePattern.exec('abab');
console.log(match); // ['abab'] (没有子组)

// 实际应用:重复模式但不捕获
const phonePattern = /(?:\d{3}-){2}\d{4}/;
console.log(phonePattern.test('123-456-7890')); // true

3. 命名捕获组

// (?<name>...) - 命名捕获组
const namedGroupPattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const date = '2023-12-25';
const match = date.match(namedGroupPattern);

console.log(match.groups);
// { year: '2023', month: '12', day: '25' }

// 访问命名组
console.log(match.groups.year);  // '2023'
console.log(match.groups.month); // '12'
console.log(match.groups.day);   // '25'

// 在replace中使用命名组
const formatted = date.replace(
    namedGroupPattern,
    '$<day>/$<month>/$<year>'
);
console.log(formatted); // '25/12/2023'

🔍 选择和分支

1. 或操作符

// | - 或操作符
const orPattern = /cat|dog/;
console.log(orPattern.test('I have a cat'));    // true
console.log(orPattern.test('I have a dog'));    // true
console.log(orPattern.test('I have a bird'));   // false

// 组合使用
const animalPattern = /(cat|dog|bird)s?/;
console.log(animalPattern.test('cat'));    // true
console.log(animalPattern.test('cats'));   // true
console.log(animalPattern.test('dogs'));   // true

2. 分支优先级

// 注意优先级问题
const problematicPattern = /cat|doggy/;
console.log(problematicPattern.test('doggy')); // false (只匹配cat或dog)

// 使用分组解决优先级
const fixedPattern = /cat|dog(gy)?/;
console.log(fixedPattern.test('doggy')); // true

// 或者在整个模式上加分组
const betterPattern = /(cat|doggy)/;
console.log(betterPattern.test('doggy')); // true

🎨 正则表达式标志

1. 常用标志

// g - 全局匹配
const globalPattern = /hello/g;
const text = 'hello world, hello everyone';
const matches = text.match(globalPattern);
console.log(matches); // ['hello', 'hello']

// i - 忽略大小写
const caseInsensitivePattern = /hello/i;
console.log(caseInsensitivePattern.test('Hello')); // true
console.log(caseInsensitivePattern.test('HELLO')); // true

// m - 多行模式
const multilinePattern = /^hello/m;
const multilineText = 'first line\nhello world';
console.log(multilinePattern.test(multilineText)); // true

// u - Unicode模式
const unicodePattern = /\u{1F600}/u; // 😀
console.log(unicodePattern.test('😀')); // true

// y - 粘性模式
const stickyPattern = /hello/y;
const stickyText = 'hello world';
stickyPattern.lastIndex = 6;
console.log(stickyPattern.test(stickyText)); // false (必须从指定位置开始匹配)

2. 组合使用标志

// 组合多个标志
const combinedPattern = /hello/gi;
const text = 'Hello world, hello everyone, HELLO!';
const matches = text.match(combinedPattern);
console.log(matches); // ['Hello', 'hello', 'HELLO']

🚀 实际应用场景

1. 表单验证

// 邮箱验证
function validateEmail(email) {
    const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    return emailPattern.test(email);
}

console.log(validateEmail('user@example.com'));    // true
console.log(validateEmail('user.name@example.co.uk')); // true
console.log(validateEmail('invalid-email'));       // false

// 手机号验证(中国)
function validateChinesePhone(phone) {
    const phonePattern = /^1[3-9]\d{9}$/;
    return phonePattern.test(phone);
}

console.log(validateChinesePhone('13812345678')); // true
console.log(validateChinesePhone('12345678901')); // false

// 密码强度验证
function validatePassword(password) {
    // 至少8位,包含大小写字母、数字和特殊字符
    const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
    return passwordPattern.test(password);
}

console.log(validatePassword('Password123!')); // true
console.log(validatePassword('weak'));        // false

2. 数据提取

// 提取URL参数
function extractUrlParams(url) {
    const paramPattern = /[?&]([^=]+)=([^&]*)/g;
    const params = {};
    let match;

    while ((match = paramPattern.exec(url)) !== null) {
        params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
    }

    return params;
}

const url = 'https://example.com/search?q=javascript&lang=en&page=1';
console.log(extractUrlParams(url));
// { q: 'javascript', lang: 'en', page: '1' }

// 提取Markdown链接
function extractMarkdownLinks(text) {
    const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
    const links = [];
    let match;

    while ((match = linkPattern.exec(text)) !== null) {
        links.push({
            text: match[1],
            url: match[2]
        });
    }

    return links;
}

const markdownText = 'Visit [Google](https://google.com) and [GitHub](https://github.com)';
console.log(extractMarkdownLinks(markdownText));
// [
//   { text: 'Google', url: 'https://google.com' },
//   { text: 'GitHub', url: 'https://github.com' }
// ]

3. 数据清洗

// 清理空白字符
function cleanWhitespace(str) {
    // 移除开头和结尾的空白,并将多个空白字符替换为单个空格
    return str.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
}

console.log(cleanWhitespace('  hello   world  ')); // 'hello world'

// 移除HTML标签
function stripHtml(html) {
    return html.replace(/<[^>]*>/g, '');
}

console.log(stripHtml('<p>Hello <strong>world</strong>!</p>')); // 'Hello world!'

// 格式化电话号码
function formatPhoneNumber(phone) {
    // 移除非数字字符,然后格式化
    const cleaned = phone.replace(/\D/g, '');
    const phonePattern = /(\d{3})(\d{4})(\d{4})/;
    return cleaned.replace(phonePattern, '$1-$2-$3');
}

console.log(formatPhoneNumber('13812345678')); // '138-1234-5678'
console.log(formatPhoneNumber('(138) 1234-5678')); // '138-1234-5678'

4. 文本搜索和替换

// 高亮搜索关键词
function highlightKeywords(text, keywords) {
    const pattern = new RegExp(`(${keywords.join('|')})`, 'gi');
    return text.replace(pattern, '<mark>$1</mark>');
}

const searchText = 'JavaScript is a programming language. JavaScript is popular.';
console.log(highlightKeywords(searchText, ['JavaScript', 'programming']));
// '<mark>JavaScript</mark> is a <mark>programming</mark> language. <mark>JavaScript</mark> is popular.'

// 批量替换
function multipleReplacements(text, replacements) {
    return replacements.reduce((result, [pattern, replacement]) => {
        return result.replace(new RegExp(pattern, 'g'), replacement);
    }, text);
}

const originalText = 'Hello world! This is a test. Hello again!';
const result = multipleReplacements(originalText, [
    ['Hello', 'Hi'],
    ['world', 'universe'],
    ['test', 'example']
]);
console.log(result); // 'Hi universe! This is a example. Hi again!'

🛠️ 高级技巧

1. 环视断言

// 正向先行断言 (?=...) - 匹配后面跟着特定内容的位置
const positiveLookahead = /Windows(?= XP|7|10)/;
console.log(positiveLookahead.test('Windows 10')); // true
console.log(positiveLookahead.test('Windows 98')); // false

// 负向先行断言 (?!) - 匹配后面不跟着特定内容的位置
const negativeLookahead = /Windows(?! XP|7|10)/;
console.log(negativeLookahead.test('Windows 98')); // true
console.log(negativeLookahead.test('Windows 10')); // false

// 正向后行断言 (?<=...) - 匹配前面有特定内容的位置
const positiveLookbehind = /(?<=\$)\d+/;
console.log(positiveLookbehind.exec('Price: $100')); // ['100']

// 负向后行断言 (?<!...) - 匹配前面没有特定内容的位置
const negativeLookbehind = /(?<!\$)\d+/;
console.log(negativeLookbehind.exec('Price: 100')); // ['100']

2. 条件匹配

// 使用条件匹配(ES2018+)
const conditionalPattern = /(a)?b(?(1)c|d)/;
console.log(conditionalPattern.test('abc')); // true (有a时必须有c)
console.log(conditionalPattern.test('abd')); // true (没有a时必须有d)
console.log(conditionalPattern.test('ab'));  // false

3. 性能优化

// 避免灾难性回溯
// ❌ 容易导致回溯的模式
const badPattern = /^(a+)+b$/;
console.time('bad');
console.log(badPattern.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac')); // false
console.timeEnd('bad');

// ✅ 优化后的模式
const goodPattern = /^a+b$/;
console.time('good');
console.log(goodPattern.test('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac')); // false
console.timeEnd('good');

// 使用原子组防止回溯
const atomicPattern = /^(?>a+)+b$/;

📝 常用正则表达式模式

1. 用户名验证

const usernamePattern = /^[a-zA-Z0-9_]{3,20}$/;
// 3-20位,只能包含字母、数字和下划线

2. IP地址验证

const ipv4Pattern = /^(?:(?: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]?)$/;

3. URL验证

const urlPattern = /^https?:\/\/(?:[-\w.])+(?:\:[0-9]+)?(?:\/(?:[\w\/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?$/;

4. 颜色值验证

const hexColorPattern = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
const rgbColorPattern = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;

⚠️ 常见陷阱

1. 忘记转义特殊字符

// ❌ 错误:. 被当作通配符
const wrongPattern = /example.com/;
console.log(wrongPattern.test('examplexcom')); // true (不期望的结果)

// ✅ 正确:转义特殊字符
const correctPattern = /example\.com/;
console.log(correctPattern.test('example.com')); // true
console.log(correctPattern.test('examplexcom')); // false

2. 贪婪匹配问题

const html = '<div>content1</div><div>content2</div>';

// ❌ 贪婪匹配可能匹配过多
const greedyMatch = html.match(/<div>.*<\/div>/);
console.log(greedyMatch[0]); // '<div>content1</div><div>content2</div>'

// ✅ 使用惰性匹配
const lazyMatch = html.match(/<div>.*?<\/div>/g);
console.log(lazyMatch); // ['<div>content1</div>', '<div>content2</div>']

3. 全局标志下的lastIndex问题

const pattern = /test/g;
const str = 'test test';

console.log(pattern.test(str)); // true
console.log(pattern.lastIndex); // 4

console.log(pattern.test(str)); // true
console.log(pattern.lastIndex); // 9

console.log(pattern.test(str)); // false
console.log(pattern.lastIndex); // 0 (重置)

// 解决方案:不使用全局标志或重置lastIndex
pattern.lastIndex = 0;

📝 最佳实践

  1. 使用字面量创建静态正则表达式:更简洁且性能更好
  2. 转义特殊字符:在动态构建正则时使用RegExp.escape
  3. 合理使用捕获组:非必要时不捕获,使用非捕获组
  4. 避免回溯:避免嵌套量词导致的性能问题
  5. 添加注释:复杂正则表达式需要详细注释
  6. 测试边界情况:测试各种输入,包括极端情况

🎯 小结

  • 掌握了正则表达式的基本语法和元字符
  • 学会了创建和使用正则表达式
  • 理解了正则表达式的匹配模式和标志
  • 掌握了常见正则表达式模式和应用场景
  • 了解了性能优化和常见陷阱

本模块学习完成! 🎉