重提正则表达式

245 阅读13分钟

前端正则表达式完全指南

正则表达式(Regular Expression,常简写为 RegExp、regex 或 RE)是用于匹配字符串中字符组合的模式。 [1] 在 JavaScript 中,正则表达式也是对象。它们是前端开发中不可或缺的工具,广泛应用于表单验证、文本搜索与替换、数据提取等多种场景。 [1][2]

一、正则表达式基础

1. 什么是正则表达式?

正则表达式是一种描述字符模式的对象,它定义了字符串的匹配规则。 [2] 通过这些规则,我们可以:

  • 验证:检查一个字符串是否符合特定的格式(如邮箱、手机号)。
  • 搜索:在文本中查找符合规则的子字符串。
  • 替换:找到匹配的子字符串并将其替换为其他内容。
  • 提取:从字符串中获取符合特定模式的部分。

2. 创建正则表达式

在 JavaScript 中,有两种创建正则表达式的方式:

  • 字面量创建:这是最常见和推荐的方式,语法是在两个斜杠 / 之间包住正则表达式的模式。 [3][4]

    let regexLiteral = /abc/i; // 匹配 "abc",忽略大小写
    
  • 构造函数创建:使用 RegExp 对象的构造函数。当正则表达式的模式是动态生成的时候,这种方式非常有用。 [4][5]

    let patternString = "abc";
    let flags = "i";
    let regexConstructor = new RegExp(patternString, flags); // 同上
    // 注意:如果模式字符串中包含特殊字符(如 \),需要进行转义,例如 new RegExp("\d+")
    

3. 修饰符(Flags)

修饰符用于指定额外的匹配策略,它们写在正则表达式的斜杠之后(字面量)或作为 RegExp 构造函数的第二个参数。 [2][6]

  • i (ignoreCase): 忽略大小写匹配。 [2]
  • g (global): 全局匹配,查找所有匹配项,而不是在找到第一个匹配后停止。 [2]
  • m (multiline): 多行匹配。使 ^$ 能够匹配每一行的开始和结束,而不仅仅是整个字符串的开始和结束。 [2]
  • u (unicode): 启用 Unicode 支持,正确处理四个字节的 UTF-16 编码。
  • y (sticky): 粘性匹配,从 lastIndex 属性指定的位置开始匹配,并且要求匹配必须从该位置开始。
  • s (dotAll): 使得元字符 . 可以匹配任何单个字符,包括换行符 \n

4. 元字符(Metacharacters)

元字符是在正则表达式中具有特殊含义的字符,它们不是按字面意义进行匹配。 [3]

  • 单个字符匹配:

    • . : 匹配除换行符(\n)以外的任何单个字符(除非使用了 s 修饰符)。 [3]
    • \d : 匹配一个数字字符。等价于 [0-9][3]
    • \D : 匹配一个非数字字符。等价于 [^0-9][3]
    • \w : 匹配任何字母数字字符(字母、数字和下划线)。等价于 [A-Za-z0-9_][1][3]
    • \W : 匹配任何非字母数字字符。等价于 [^A-Za-z0-9_][1]
    • \s : 匹配任何空白字符(包括空格、制表符、换页符等)。 [3]
    • \S : 匹配任何非空白字符。 [1]
    • \t : 匹配一个制表符。 [1]
    • \n : 匹配一个换行符。 [3]
  • 量词(Quantifiers) : 用于指定前面一个字符或子表达式出现的次数。 [3]

    • * : 匹配前面的字符或子表达式零次或多次。等价于 {0,}[3]
    • + : 匹配前面的字符或子表达式一次或多次。等价于 {1,}[3]
    • ? : 匹配前面的字符或子表达式零次或一次。等价于 {0,1}。也用于声明非贪婪匹配。 [3]
    • {n} : 匹配前面的字符或子表达式恰好 n 次。 [3]
    • {n,} : 匹配前面的字符或子表达式至少 n 次。 [3]
    • {n,m} : 匹配前面的字符或子表达式至少 n 次,但不超过 m 次。 [3]
  • 锚点(Anchors) : 用于指定匹配发生的位置。

    • ^ : 匹配输入的开始。如果开启了多行模式 m,也会匹配行的开始。 [3]
    • $ : 匹配输入的结束。如果开启了多行模式 m,也会匹配行的结束。 [3]
    • \b : 匹配一个单词边界。单词边界是单词字符 (\w) 和非单词字符 (\W) 之间的位置,或字符串的开始/结束与单词字符之间的位置。 [1][7]
    • \B : 匹配一个非单词边界。 [1]
  • 字符类(Character Classes) :

    • [xyz] : 字符集合。匹配方括号中任何一个字符。例如,[abc] 匹配 "a" 或 "b" 或 "c"。 [3]
    • [^xyz] : 否定字符集合。匹配任何不在方括号中的字符。例如,[^abc] 匹配任何不是 "a"、"b" 或 "c" 的字符。 [2][3]
    • [a-z] : 范围。匹配指定范围内的任何字符。例如,[a-z] 匹配任何小写字母。 [3]
  • 分组与捕获(Grouping and Capturing) :

    • (x) : 捕获组。匹配 x 并且记住匹配项。括号创建的子串(也叫捕获组)可以在结果中被引用。 [8][9]
    • (?:x) : 非捕获组。匹配 x 但是不记住匹配项。这对于需要分组但不需要在结果中单独引用的部分很有用,可以提高效率。 [8][10]
    • \n : 反向引用。匹配一个之前捕获到的第 n 个分组的内容。例如,\1 匹配第一个捕获组的内容。 [1][11]
  • 或操作符:

    • x|y : 匹配 xy[3]
  • 转义字符:

    • `` : 用于转义特殊字符,使其按字面意义匹配。例如,要匹配 . 字符本身,需要使用 .

5. 贪婪匹配与非贪婪匹配

默认情况下,量词是“贪婪的”,它们会尽可能多地匹配文本。 [12]

  • 贪婪匹配: *, +, {n,} 会匹配尽可能多的字符。
    例如,对于字符串 "<div>abc</div><div>def</div>",正则表达式 /<.*>/ 会匹配整个字符串 <div>abc</div><div>def</div>
  • 非贪婪匹配(懒惰匹配) : 在量词后面加上一个问号 ? 可以使其变为非贪婪的,它会尽可能少地匹配文本。 [12]
    例如,对于相同字符串,/<.*?>/ 会匹配 <div> 两次(第一次是 <div>abc</div>,第二次是 <div>def</div>,如果使用全局匹配)。更准确地说,它会匹配 <div></div> 分别两次,如果内容更复杂。如果目标是匹配整个标签对,则需要更精确的模式,如 /<div>.*?</div>/

二、在 JavaScript 中使用正则表达式

JavaScript 提供了两种主要方式来使用正则表达式:通过 RegExp 对象的方法,以及通过 String 对象的方法。 [1]

1. RegExp 对象的方法

  • test(string) :

    • 测试一个字符串是否匹配该正则表达式。 [13][14]
    • 如果匹配成功,返回 true;否则返回 false[14]
    • 这是最简单也通常是性能最好的方法,当你只需要知道是否匹配时。
    const str = "hello world";
    const regex = /hello/;
    console.log(regex.test(str)); // 输出: true
    
    const regexNoMatch = /javascript/;
    console.log(regexNoMatch.test(str)); // 输出: false
    
  • exec(string) :

    • 在一个指定的字符串中执行搜索匹配。 [13][15]

    • 如果匹配成功,返回一个数组,其中第一个元素是整个匹配的字符串,后续元素是捕获组匹配的子串。该数组还有额外的属性:index (匹配项在字符串中的起始索引) 和 input (原始被测试的字符串)。 [1][15]

    • 如果匹配失败,返回 null[15]

    • 当正则表达式设置了全局标志 g 时,exec() 的行为会比较特殊:

      • 它会从 lastIndex 属性指定的位置开始搜索。
      • 如果找到匹配,它会更新 lastIndex 到匹配文本末尾的下一个位置。
      • 下次再调用 exec() 时,会从新的 lastIndex 开始。
      • 如果再也找不到匹配,它会返回 null,并将 lastIndex 重置为 0。
    const str = "JavaScript is fun, JavaScript is powerful.";
    const regex = /JavaScript/g; // 全局匹配
    let match;
    while ((match = regex.exec(str)) !== null) {
      console.log(`Found "${match[0]}" at index ${match.index}. Next search starts at ${regex.lastIndex}.`);
    }
    // 输出:
    // Found "JavaScript" at index 0. Next search starts at 10.
    // Found "JavaScript" at index 20. Next search starts at 30.
    
    const regexWithGroup = /Java(Script)/;
    const result = regexWithGroup.exec("JavaScript");
    if (result) {
        console.log(result[0]); // "JavaScript" (整个匹配)
        console.log(result[1]); // "Script" (第一个捕获组)
        console.log(result.index); // 0
        console.log(result.input); // "JavaScript"
    }
    

2. String 对象的方法

这些是字符串原型上的方法,它们接受正则表达式作为参数。 [1][16]

  • match(regexp) :

    • 检索字符串与正则表达式的匹配结果。 [16][17]
    • 如果正则表达式没有全局标志 gmatch() 的行为与 RegExp.prototype.exec() 类似,返回第一个匹配及其捕获组,失败则返回 null[18]
    • 如果正则表达式设置了全局标志 gmatch() 会返回一个包含所有匹配子串的数组(不包含捕获组信息),如果没有任何匹配,则返回 null[3][18][19]
    const str = "The rain in SPAIN stays mainly in the plain";
    const regexGlobal = /ain/g;
    console.log(str.match(regexGlobal)); // 输出: ["ain", "ain", "ain"]
    
    const regexNonGlobal = /ain/;
    const matchResult = str.match(regexNonGlobal);
    console.log(matchResult[0]);    // "ain"
    console.log(matchResult.index); // 5
    console.log(matchResult.input); // "The rain in SPAIN stays mainly in the plain"
    
    const strNoMatch = "Hello World";
    console.log(strNoMatch.match(/xyz/g)); // null
    
  • matchAll(regexp) :

    • 返回一个迭代器,该迭代器包含了字符串与一个设置了全局标志 g 的正则表达式的所有匹配结果(包括捕获组)。 [1][19]
    • 如果正则表达式没有 g 标志,会抛出 TypeError
    • 每个匹配结果的格式与 exec() 返回的数组相同。
    const str = "Test1 Test2 Test3";
    const regex = /Test(\d)/g; // 必须有 g 标志
    const matches = str.matchAll(regex);
    
    for (const match of matches) {
      console.log(`Full match: ${match[0]}, Group 1: ${match[1]}, Index: ${match.index}`);
    }
    // 输出:
    // Full match: Test1, Group 1: 1, Index: 0
    // Full match: Test2, Group 1: 2, Index: 6
    // Full match: Test3, Group 1: 3, Index: 12
    
  • search(regexp) :

    • 执行正则表达式和字符串之间的搜索匹配。 [1][20]
    • 如果匹配成功,返回第一个匹配项在字符串中的索引。
    • 如果匹配失败,返回 -1
    • search() 方法不执行全局匹配,它忽略 g 标志,并且总是从字符串的开头进行搜索。
    const str = "JavaScript is fun!";
    const regex = /is/;
    console.log(str.search(regex)); // 输出: 11 ( "is" 在 "JavaScript is fun!" 中的索引)
    
    console.log(str.search(/xyz/)); // 输出: -1
    
  • replace(regexp|substr, newSubstr|function) :

    • 用指定的替换内容替换字符串中与正则表达式(或子串)匹配的部分。 [1][16]

    • 返回一个新的字符串,原始字符串不变。

    • 如果第一个参数是正则表达式且设置了 g 标志,则替换所有匹配项;否则只替换第一个匹配项。

    • 第二个参数可以是字符串,也可以是一个函数。

      • 字符串替换: 可以使用特殊替换模式:

        • $$: 插入一个 $ 字符。
        • $&: 插入匹配的子串。
        • `$``: 插入当前匹配左边的内容。
        • $': 插入当前匹配右边的内容。
        • $n: 插入第 n 个捕获组的内容(如果存在)。n 是从1开始的数字。
        • $<Name>: 插入名为 Name 的捕获组的内容(如果存在)。
      • 函数替换: 该函数会在每次匹配时被调用,其返回值将作为替换字符串。函数会接收多个参数:

        1. match: 匹配的子串(同 $&)。
        2. p1, p2, ...: 第1个、第2个...捕获组的内容(如果正则中有捕获组)。
        3. offset: 匹配的子串在原字符串中的偏移量(索引)。
        4. string: 被操作的原始字符串。
        5. namedCaptures (可选): 一个包含命名捕获组的对象(如果正则中有命名捕获组)。
    const str = "Mr Blue has a blue house and a blue car";
    const newStr1 = str.replace(/blue/g, "red");
    console.log(newStr1); // "Mr Blue has a red house and a red car" (注意 "Blue" 未被替换)
    
    const newStr2 = str.replace(/blue/ig, "red"); // i 忽略大小写
    console.log(newStr2); // "Mr red has a red house and a red car"
    
    // 使用捕获组和特殊替换模式
    const nameStr = "Doe, John";
    const reorderedName = nameStr.replace(/(\w+),\s*(\w+)/, "$2 $1");
    console.log(reorderedName); // "John Doe"
    
    // 使用函数替换
    const celsiusTemps = "Temp: 15C, 23C, 0C";
    const fahrenheitTemps = celsiusTemps.replace(/(\d+)C\b/g, (match, p1_temp) => {
        const celsius = parseFloat(p1_temp);
        const fahrenheit = (celsius * 9/5) + 32;
        return `${fahrenheit}F`;
    });
    console.log(fahrenheitTemps); // "Temp: 59F, 73.4F, 32F"
    
  • split(separator, limit) :

    • 使用指定的分隔符(可以是字符串或正则表达式)将字符串分割成一个子字符串数组。 [1][17]
    • 返回一个包含子字符串的数组。
    • limit (可选): 一个整数,限定返回的分割片段数量。
    const str = "apple,banana;orange kiwi";
    const fruits1 = str.split(/[,;\s]+/); // 分隔符可以是逗号、分号或一个或多个空格
    console.log(fruits1); // ["apple", "banana", "orange", "kiwi"]
    
    const csvData = "col1,col2,col3";
    const headers = csvData.split(',', 2); // 限制数量
    console.log(headers); // ["col1", "col2"]
    

三、前端常用正则表达式示例代码详解

下面我们将通过一个详尽的表单验证示例来展示正则表达式在前端的应用。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>正则表达式表单验证示例</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
        .container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
        h1 { text-align: center; color: #333; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input[type="text"], input[type="email"], input[type="password"], input[type="tel"], input[type="url"], input[type="date"] {
            width: calc(100% - 22px);
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
        }
        input:focus { border-color: #007bff; outline: none; }
        .error-message { color: red; font-size: 0.9em; margin-top: 3px; }
        .success-message { color: green; font-size: 0.9em; margin-top: 3px; }
        button {
            background-color: #007bff;
            color: white;
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 1em;
            display: block;
            width: 100%;
            margin-top: 20px;
        }
        button:hover { background-color: #0056b3; }
        /* 响应式调整 */
        @media (max-width: 600px) {
            input[type="text"], input[type="email"], input[type="password"], input[type="tel"], input[type="url"], input[type="date"] {
                width: 100%; /* 在窄屏上占满宽度,因为padding已通过box-sizing处理 */
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>用户注册</h1>
        <form id="registrationForm">
            <div class="form-group">
                <label for="username">用户名:</label>
                <input type="text" id="username" name="username">
                <div class="error-message" id="usernameError"></div>
            </div>

            <div class="form-group">
                <label for="email">电子邮箱:</label>
                <input type="email" id="email" name="email">
                <div class="error-message" id="emailError"></div>
            </div>

            <div class="form-group">
                <label for="password">密码:</label>
                <input type="password" id="password" name="password">
                <div class="error-message" id="passwordError"></div>
            </div>

            <div class="form-group">
                <label for="confirmPassword">确认密码:</label>
                <input type="password" id="confirmPassword" name="confirmPassword">
                <div class="error-message" id="confirmPasswordError"></div>
            </div>

            <div class="form-group">
                <label for="phone">手机号码 (中国大陆):</label>
                <input type="tel" id="phone" name="phone">
                <div class="error-message" id="phoneError"></div>
            </div>

            <div class="form-group">
                <label for="website">个人网站 (可选):</label>
                <input type="url" id="website" name="website" placeholder="https://example.com">
                <div class="error-message" id="websiteError"></div>
            </div>

            <div class="form-group">
                <label for="birthdate">出生日期 (YYYY-MM-DD):</label>
                <input type="text" id="birthdate" name="birthdate" placeholder="YYYY-MM-DD">
                <div class="error-message" id="birthdateError"></div>
            </div>

            <button type="submit">注册</button>
        </form>
    </div>

    <script>
        // 获取表单和输入元素
        const form = document.getElementById('registrationForm');
        const usernameInput = document.getElementById('username');
        const emailInput = document.getElementById('email');
        const passwordInput = document.getElementById('password');
        const confirmPasswordInput = document.getElementById('confirmPassword');
        const phoneInput = document.getElementById('phone');
        const websiteInput = document.getElementById('website');
        const birthdateInput = document.getElementById('birthdate');

        // 正则表达式定义 [4, 15, 20, 26]
        const regexPatterns = {
            // 用户名: 4-16位,只能包含字母、数字、下划线、连字符
            username: /^[a-zA-Z0-9_-]{4,16}$/,
            // 电子邮箱: 标准邮箱格式
            email: /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*.[a-zA-Z]{2,}$/,
            // 密码强度: 至少8位,包含至少一个大写字母,一个小写字母,一个数字和一个特殊字符
            password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&_#-])[A-Za-z\d@$!%*?&_#-]{8,}$/,
            // 手机号码 (中国大陆): 以1开头,第二位是3-9,后面9位数字
            phone: /^1[3-9]\d{9}$/,
            // 网址 (简单校验): 必须以 http:// 或 https:// 开头,后面跟域名和可选路径
            website: /^(https?://)?([\da-z.-]+).([a-z.]{2,6})([/\w .-]*)*/?$/,
            // 日期 (YYYY-MM-DD): 严格匹配年份、月份和日期,考虑闰年等会更复杂,这里简化
            // 简单格式: YYYY-MM-DD 或 YYYY/MM/DD
            // 更精确的日期正则会非常复杂,通常建议使用日期库进行验证
            birthdate: /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/
        };

        // 辅助函数:显示错误信息
        function showError(inputElement, message) {
            const errorElement = document.getElementById(inputElement.id + 'Error');
            errorElement.textContent = message;
            errorElement.className = 'error-message'; // 确保是错误样式
            inputElement.style.borderColor = 'red';
        }

        // 辅助函数:显示成功信息(或清除错误)
        function showSuccess(inputElement) {
            const errorElement = document.getElementById(inputElement.id + 'Error');
            errorElement.textContent = '✓'; // 或者空字符串,或者具体的成功提示
            errorElement.className = 'success-message'; // 改为成功样式
            inputElement.style.borderColor = 'green';
        }

        // 辅助函数:清除消息
        function clearMessage(inputElement) {
             const errorElement = document.getElementById(inputElement.id + 'Error');
            errorElement.textContent = '';
            inputElement.style.borderColor = '#ddd'; // 恢复默认边框
        }


        // 验证函数
        function validateUsername() {
            const value = usernameInput.value.trim();
            if (value === '') {
                showError(usernameInput, '用户名不能为空。');
                return false;
            }
            if (!regexPatterns.username.test(value)) {
                showError(usernameInput, '用户名格式无效 (4-16位,只能包含字母、数字、下划线、连字符)。');
                return false;
            }
            showSuccess(usernameInput);
            return true;
        }

        function validateEmail() {
            const value = emailInput.value.trim();
            if (value === '') {
                showError(emailInput, '电子邮箱不能为空。');
                return false;
            }
            if (!regexPatterns.email.test(value)) {
                showError(emailInput, '电子邮箱格式无效。');
                return false;
            }
            showSuccess(emailInput);
            return true;
        }

        function validatePassword() {
            const value = passwordInput.value; // 密码通常不 trim() 开头或结尾的空格
            if (value === '') {
                showError(passwordInput, '密码不能为空。');
                return false;
            }
            if (!regexPatterns.password.test(value)) {
                showError(passwordInput, '密码强度不足 (至少8位,含大小写字母、数字和特殊字符@$!%*?&_#-)');
                return false;
            }
            showSuccess(passwordInput);
            // 如果确认密码字段有内容,也需要重新验证确认密码
            if (confirmPasswordInput.value !== '') {
                validateConfirmPassword();
            }
            return true;
        }

        function validateConfirmPassword() {
            const passwordValue = passwordInput.value;
            const confirmPasswordValue = confirmPasswordInput.value;
            if (confirmPasswordValue === '') {
                showError(confirmPasswordInput, '请再次输入密码。');
                return false;
            }
            if (passwordValue !== confirmPasswordValue) {
                showError(confirmPasswordInput, '两次输入的密码不一致。');
                return false;
            }
            showSuccess(confirmPasswordInput);
            return true;
        }

        function validatePhone() {
            const value = phoneInput.value.trim();
            if (value === '') {
                showError(phoneInput, '手机号码不能为空。');
                return false;
            }
            if (!regexPatterns.phone.test(value)) {
                showError(phoneInput, '手机号码格式无效 (中国大陆11位数字)。');
                return false;
            }
            showSuccess(phoneInput);
            return true;
        }

        function validateWebsite() {
            const value = websiteInput.value.trim();
            if (value === '') { // 可选字段,为空时清除消息并通过验证
                clearMessage(websiteInput);
                return true;
            }
            if (!regexPatterns.website.test(value)) {
                showError(websiteInput, '网址格式无效 (例如: https://example.com)。');
                return false;
            }
            showSuccess(websiteInput);
            return true;
        }

        function validateBirthdate() {
            const value = birthdateInput.value.trim();
            if (value === '') {
                showError(birthdateInput, '出生日期不能为空。');
                return false;
            }
            if (!regexPatterns.birthdate.test(value)) {
                // 进一步检查日期有效性,例如 2023-02-30 是无效的
                // 这超出了简单正则的范围,通常需要日期库或更复杂的逻辑
                const parts = value.split('-');
                let isValidDate = false;
                if (parts.length === 3) {
                    const year = parseInt(parts[0], 10);
                    const month = parseInt(parts[1], 10); // month is 1-12
                    const day = parseInt(parts[2], 10);
                    // 简单的日期有效性检查 (不完美,但比纯正则好)
                    // 注意:JavaScript Date对象的月份是0-11
                    const dateObj = new Date(year, month - 1, day);
                    if (dateObj && dateObj.getFullYear() === year && dateObj.getMonth() === month - 1 && dateObj.getDate() === day) {
                         // 还可以添加年龄限制等
                        const today = new Date();
                        const eighteenYearsAgo = new Date(today.getFullYear() - 18, today.getMonth(), today.getDate());
                        if (dateObj > today) {
                            showError(birthdateInput, '出生日期不能是未来日期。');
                            return false;
                        } else if (dateObj > eighteenYearsAgo) {
                            // 示例:简单判断是否未满18岁
                            // showError(birthdateInput, '用户必须年满18岁。');
                            // return false;
                        }
                        isValidDate = true;
                    }
                }

                if (!isValidDate && !regexPatterns.birthdate.test(value)) { // 如果日期对象验证也失败,且正则也失败
                     showError(birthdateInput, '出生日期格式无效 (YYYY-MM-DD),或日期不存在。');
                     return false;
                } else if (!isValidDate && regexPatterns.birthdate.test(value)) { // 正则通过,但日期对象验证失败(如2月30日)
                     showError(birthdateInput, '出生日期无效 (例如,日期不存在如2月30日)。');
                     return false;
                }
            }
            showSuccess(birthdateInput);
            return true;
        }

        // 添加事件监听器 (实时验证或失去焦点时验证)
        usernameInput.addEventListener('blur', validateUsername);
        emailInput.addEventListener('blur', validateEmail);
        passwordInput.addEventListener('blur', validatePassword);
        // 密码输入时,如果确认密码有值,也触发确认密码的验证
        passwordInput.addEventListener('input', () => {
            if (confirmPasswordInput.value) {
                validateConfirmPassword();
            }
        });
        confirmPasswordInput.addEventListener('blur', validateConfirmPassword);
        confirmPasswordInput.addEventListener('input', validateConfirmPassword); // 实时匹配
        phoneInput.addEventListener('blur', validatePhone);
        websiteInput.addEventListener('blur', validateWebsite);
        birthdateInput.addEventListener('blur', validateBirthdate);

        // 表单提交事件
        form.addEventListener('submit', function(event) {
            event.preventDefault(); // 阻止表单默认提交行为

            // 执行所有验证
            const isUsernameValid = validateUsername();
            const isEmailValid = validateEmail();
            const isPasswordValid = validatePassword();
            const isConfirmPasswordValid = validateConfirmPassword();
            const isPhoneValid = validatePhone();
            const isWebsiteValid = validateWebsite(); // 可选字段,其验证函数内部会处理空值
            const isBirthdateValid = validateBirthdate();

            if (isUsernameValid && isEmailValid && isPasswordValid && isConfirmPasswordValid && isPhoneValid && isWebsiteValid && isBirthdateValid) {
                alert('表单提交成功!');
                // 在这里可以执行实际的表单提交操作,例如使用 fetch API 发送数据到服务器
                // form.submit(); // 如果要进行传统的表单提交
                // 或者收集数据:
                const formData = {
                    username: usernameInput.value.trim(),
                    email: emailInput.value.trim(),
                    password: passwordInput.value, // 通常密码不trim
                    phone: phoneInput.value.trim(),
                    website: websiteInput.value.trim(),
                    birthdate: birthdateInput.value.trim()
                };
                console.log("表单数据:", formData);
                // 实际项目中,这里会发送 formData 到服务器
            } else {
                alert('表单包含错误,请检查后重试。');
            }
        });

        // 正则表达式解释 (这部分可以作为注释或文档)
        console.log("--- 正则表达式详解 ---");
        console.log("用户名 (username): ", regexPatterns.username.source);
        // ^[a-zA-Z0-9_-]{4,16}$
        // ^: 字符串开始
        // [a-zA-Z0-9_-]: 允许小写字母、大写字母、数字、下划线、连字符
        // {4,16}: 以上字符出现4到16次
        // $: 字符串结束

        console.log("电子邮箱 (email): ", regexPatterns.email.source);
        // ^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*.[a-zA-Z]{2,}$
        // ^: 字符串开始
        // [a-zA-Z0-9_.-]+: 用户名部分,允许字母、数字、下划线、点、连字符,至少一个
        // @: @符号
        // [a-zA-Z0-9-]+: 域名第一部分,允许字母、数字、连字符,至少一个
        // (.[a-zA-Z0-9-]+)*: 可选的子域名,如 .sub.domain,*表示0个或多个
        // .: 点符号
        // [a-zA-Z]{2,}: 顶级域名,至少2个字母 (如 com, cn, org)
        // $: 字符串结束

        console.log("密码强度 (password): ", regexPatterns.password.source);
        // ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&_#-])[A-Za-z\d@$!%*?&_#-]{8,}$
        // ^: 字符串开始
        // (?=.*[a-z]): 正向先行断言,表示字符串中必须包含至少一个小写字母 [6, 38]
        // (?=.*[A-Z]): 正向先行断言,表示字符串中必须包含至少一个大写字母
        // (?=.*\d): 正向先行断言,表示字符串中必须包含至少一个数字
        // (?=.*[@$!%*?&_#-]): 正向先行断言,表示字符串中必须包含至少一个特殊字符(列表中的一个)
        // [A-Za-z\d@$!%*?&_#-]{8,}: 密码本身由字母、数字和指定的特殊字符组成,长度至少为8位
        // $: 字符串结束

        console.log("手机号码 (phone): ", regexPatterns.phone.source);
        // ^1[3-9]\d{9}$
        // ^: 字符串开始
        // 1: 以数字1开头
        // [3-9]: 第二位是3到9之间的数字
        // \d{9}: 后面跟9个数字
        // $: 字符串结束

        console.log("网址 (website): ", regexPatterns.website.source);
        // ^(https?://)?([\da-z.-]+).([a-z.]{2,6})([/\w .-]*)*/?$
        // ^: 字符串开始
        // (https?://)? : 可选的协议 (http:// 或 https://)
        //      http: "http"
        //      s?: 可选的 "s"
        //      ://: "://"
        //      (...)? : 整个组是可选的
        // ([\da-z.-]+): 域名主体,至少一个数字、小写字母、点或连字符
        // .: 点
        // ([a-z.]{2,6}): 顶级域名,2到6个小写字母或点 (如 .com, .co.uk)
        // ([/\w .-]*)*: 可选的路径、查询参数等。允许斜杠、单词字符、点、空格、连字符,0次或多次。
        // /? : 可选的末尾斜杠
        // $: 字符串结束

        console.log("出生日期 (birthdate): ", regexPatterns.birthdate.source);
        // ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
        // ^: 字符串开始
        // \d{4}: 四位数字年份
        // -: 连字符
        // (0[1-9]|1[0-2]): 月份。01-09 或 10-12
        //      0[1-9]: 01, 02, ..., 09
        //      |: 或
        //      1[0-2]: 10, 11, 12
        // -: 连字符
        // (0[1-9]|[12]\d|3[01]): 日期。01-09 或 10-29 或 30-31
        //      0[1-9]: 01, ..., 09
        //      |: 或
        //      [12]\d: 10-19, 20-29
        //      |: 或
        //      3[01]: 30, 31
        // $: 字符串结束
        // 注意: 这个日期正则只检查格式,不检查日期的实际有效性 (如2月30日)。实际有效性检查需要额外逻辑。
    </script>
</body>
</html>

代码讲解:

  1. HTML结构:

    • 一个标准的HTML表单,包含用户名、邮箱、密码、确认密码、手机号、可选的个人网站和出生日期字段。
    • 每个输入字段后面都有一个 div 用于显示错误或成功消息。
    • 使用了简单的CSS进行样式化,使其更易于查看。
  2. JavaScript - regexPatterns 对象:

    • 这个对象集中存储了所有字段的正则表达式。 [21][22]

    • username: /^[a-zA-Z0-9_-]{4,16}$/

      • ^$ 分别表示字符串的开始和结束,确保整个字符串都匹配此模式。
      • [a-zA-Z0-9_-] 定义了允许的字符集:小写字母 (a-z),大写字母 (A-Z),数字 (0-9),下划线 (_),以及连字符 (-)。
      • {4,16} 表示前面的字符集必须出现4到16次。
    • email: /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*.[a-zA-Z]{2,}$/

      • [a-zA-Z0-9_.-]+: 邮箱的用户名部分,允许字母、数字、下划线、点、连字符,至少出现一次 (+)。
      • @: 固定的 @ 符号。
      • [a-zA-Z0-9-]+: 域名部分,允许字母、数字、连字符,至少出现一次。
      • (.[a-zA-Z0-9-]+)*: 可选的子域名部分(如 mail.google 中的 mail),可以出现零次或多次 (*)。每个子域名以点开头。
      • .[a-zA-Z]{2,}: 顶级域名,如 .com, .org,必须以点开头,后面跟至少两个字母。
    • password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&_#-])[A-Za-z\d@$!%*?&_#-]{8,}$/

      • 这是一个使用了正向先行断言 (Positive Lookahead) (?=...) 的复杂密码。 [10][23]
      • (?=.*[a-z]): 确保字符串中至少有一个小写字母。 .* 匹配任意数量的任意字符(除了换行符),后面跟着一个小写字母。这个断言本身不消耗字符,只是检查条件。
      • (?=.*[A-Z]): 确保至少一个大写字母。
      • (?=.*\d): 确保至少一个数字。
      • (?=.*[@$!%*?&_#-]): 确保至少一个特殊字符(从给定的集合中选择)。
      • [A-Za-z\d@$!%*?&_#-]{8,}: 实际匹配的密码字符。必须是字母、数字或指定的特殊字符,并且长度至少为8位。
    • phone: /^1[3-9]\d{9}$/

      • 1: 必须以数字1开头。
      • [3-9]: 第二位是3到9之间的数字。
      • \d{9}: 后面必须跟9个数字。
    • website: /^(https?://)?([\da-z.-]+).([a-z.]{2,6})([/\w .-]*)*/?$/

      • (https?://)?: 协议部分,http://https://,整个部分是可选的 (?)。 s? 表示 s 可选。 / 是转义的斜杠。
      • ([\da-z.-]+): 域名主体,例如 examplesub.example
      • .: 域名和顶级域名之间的点。
      • ([a-z.]{2,6}): 顶级域名,例如 comco.uk
      • ([/\w .-]*)*: 可选的路径和查询参数。
      • /?: 可选的末尾斜杠。
    • birthdate: /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]| [[24]](https://www.cnblogs.com/LLD-3/p/9673517.html)\d|3)$/

      • \d{4}: 四位年份。
      • -: 分隔符。
      • (0[1-9]|1[0-2]): 月份。0[1-9] 匹配 01-09,1[0-2] 匹配 10-12。
      • -: 分隔符。
      • (0[1-9]| [[24]](https://www.cnblogs.com/LLD-3/p/9673517.html)\d|3 [[1]](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_expressions)): 日期。0[1-9] 匹配 01-09, [[24]](https://www.cnblogs.com/LLD-3/p/9673517.html)\d 匹配 10-19 和 20-29,3 [[1]](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_expressions) 匹配 30, 31。
      • 注意:此日期正则仅验证格式,不验证日期的实际有效性(例如,它会允许 "2023-02-30")。在 validateBirthdate 函数中,我们添加了额外的 JavaScript Date 对象检查来处理部分这类无效日期。
  3. 辅助函数 (showError, showSuccess, clearMessage) :

    • 这些函数用于在对应的错误消息 div 中显示反馈,并改变输入框的边框颜色。
  4. 验证函数 (validateUsername, validateEmail, etc.) :

    • 每个字段都有一个对应的验证函数。
    • 首先获取输入值并 trim() (去除首尾空格,密码除外)。
    • 检查值是否为空(如果字段是必需的)。
    • 使用 regexPatterns 中对应的正则表达式的 test() 方法来验证格式。 [14]
    • 根据验证结果调用 showErrorshowSuccess
    • 返回 true (有效) 或 false (无效)。
    • validateBirthdate 函数中包含了对日期逻辑有效性的额外检查,因为单纯的正则表达式难以完美覆盖所有日期规则(如不同月份的天数、闰年)。
  5. 事件监听器:

    • 为每个输入字段添加 blur 事件监听器,当用户离开输入框时触发验证。
    • 密码字段和确认密码字段还添加了 input 事件监听器,以提供更即时的反馈。
    • 表单的 submit 事件被拦截 (event.preventDefault())。
    • 在提交时,会再次执行所有验证函数。
    • 如果所有字段都有效,则显示成功消息,并可以执行实际的提交操作(例如,通过 fetch API 将数据发送到服务器)。否则,提示用户检查错误。
  6. 正则表达式解释:

    • <script> 标签的最后,通过 console.log 输出了每个正则表达式的 .source 属性及其详细解释。这有助于理解每个模式的构成。

这个例子展示了如何在前端使用正则表达式进行常见的表单验证,并结合 JavaScript 提供用户友好的反馈。代码行数(包括HTML、CSS、JS和详细注释)已远超一般示例,力求详尽。

四、高级正则表达式特性

1. 零宽断言 (Zero-Width Assertions)

零宽断言允许你匹配基于其前面或后面的文本,而不实际消耗这些文本(即它们不包含在匹配结果中)。 [10][23]

  • 正向先行断言 (Positive Lookahead): X(?=Y)

    • 匹配 X,仅当 X 后面跟着 Y 时。Y 不会被包含在匹配结果中。
    • 例如,/Windows(?=95|98|NT|2000)/ 匹配 "Windows",但仅当它后面是 "95", "98", "NT", 或 "2000"。在密码强度校验中常用。 [23]
  • 负向先行断言 (Negative Lookahead): X(?!Y)

    • 匹配 X,仅当 X 后面不跟着 Y 时。
    • 例如,/\d+(?!%)/ 匹配一个或多个数字,但前提是这些数字后面不跟百分号 %
  • 正向后行断言 (Positive Lookbehind): (?<=Y)X (ES2018+)

    • 匹配 X,仅当 X 前面是 Y 时。Y 不会被包含在匹配结果中。
    • 例如,/(?<=$)\d+/ 匹配数字,但仅当它们前面有一个美元符号 $
  • 负向后行断言 (Negative Lookbehind): (?<!Y)X (ES2018+)

    • 匹配 X,仅当 X 前面不是 Y 时。
    • 例如,/(?<!non-)\b\w+\b/ 匹配一个单词,但前提是它前面没有 "non-"。

2. 命名捕获组 (Named Capture Groups) (ES2018+)

允许你通过名称而不是索引来引用捕获组,使代码更具可读性。 [2]
语法: (?<name>...)

const dateStr = "2025-05-29";
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const result = regex.exec(dateStr);

if (result) {
  console.log(result.groups.year);  // "2025"
  console.log(result.groups.month); // "05"
  console.log(result.groups.day);   // "29"
}
// 在 replace 方法中也可以使用命名捕获组:
// "2025-05-29".replace(regex, "$<day>/$<month>/$<year>"); // "29/05/2025"

五、正则表达式的性能与最佳实践

  • 精确匹配: 尽量使你的正则表达式更具体。避免使用过于宽泛的匹配,如 .*,如果可以用更精确的模式(如 [^"]* 来匹配双引号之间的内容)替代。 [25]

  • 避免不必要的回溯: 复杂或写得不好的正则表达式可能导致“灾难性回溯”,使得匹配时间呈指数级增长。

    • 使用非捕获组 (?:...) 当你不需要引用某个分组时。 [10]
    • 小心使用嵌套量词,特别是内部量词与外部量词可能匹配相同内容时。
    • 尽可能使模式具有确定性,减少引擎需要尝试的路径。
  • 编译与重用: 如果一个正则表达式会被多次使用,预先将其创建并存储在一个变量中,而不是在每次使用时重新创建。JavaScript引擎通常会自动优化字面量正则表达式。 [26]

  • 选择正确的工具:

    • 如果只是测试是否存在匹配,RegExp.prototype.test() 通常比 String.prototype.match()RegExp.prototype.exec() 更快。 [14]
  • 逐步构建和测试: 对于复杂的正则表达式,从简单的部分开始,逐步增加复杂性,并使用在线工具(如 Regex101, RegExr)进行测试和调试。 [27][28]

  • 考虑可读性: 虽然正则表达式以简洁著称,但过于复杂的单行正则表达式可能难以理解和维护。适当添加注释,或者将复杂的逻辑分解。

  • 了解引擎特性: 不同的正则表达式引擎(即使在不同浏览器中)可能存在细微的实现差异或性能特点。

六、动态构建正则表达式

当正则表达式的模式需要基于变量或用户输入动态生成时,必须使用 RegExp 构造函数。 [5][29]

function createSearchRegex(searchTerm, flags = "gi") {
    // 需要对 searchTerm 中的特殊正则表达式字符进行转义
    const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[]\]/g, '\$&'); // $& 表示整个被匹配的字符串
    return new RegExp(escapedSearchTerm, flags);
}

const userInput = "example.com"; // 用户可能输入包含 "." 的内容
const dynamicRegex = createSearchRegex(userInput);
console.log(dynamicRegex); // /example.com/gi

const textToSearch = "This is a test for example.com and EXAMPLE.COM.";
console.log(textToSearch.match(dynamicRegex)); // ["example.com", "EXAMPLE.COM"]

在上面的 escapedSearchTerm 中,replace(/[.*+?^${}()|[]\]/g, '\$&') 是一个关键步骤,它会转义所有在正则表达式中有特殊意义的字符,确保它们被当作普通字符进行匹配。

总结

正则表达式是前端开发中处理字符串的强大工具。 [1][30] 理解其基本语法、掌握在 JavaScript 中的使用方法、熟悉常用模式,并遵循最佳实践,可以极大地提高开发效率和代码质量。从简单的表单验证到复杂的数据提取和操作,正则表达式都能发挥重要作用。记住,实践和使用在线测试工具是精通正则表达式的关键。 [27][31]


推荐好文:

  1. 正则表达式- JavaScript - MDN Web Docs
  2. 一文详解JavaScript中的正则表达式 - 稀土掘金
  3. 前端正则最全知识汇总(学会正则收藏它就够啦)
  4. 前端必经之路:带你读懂正则表达式 - CSDN博客
  5. js如何生成动态正则表达式? 原创 - CSDN博客
  6. 前端-JS正则表达式快速入门 - 稀土掘金
  7. 【正则表达式系列】一些概念(字符组、捕获组、非捕获组) - Dailc的个人主页
  8. 用正则表达式分析URL - Harttle Land
  9. 正则表达式详解及实战 - 稀土掘金
  10. 正则表达式高级用法 - 阿里云开发者社区
  11. 正则表达式前端使用手册 - louis blog
  12. JavaScript正则表达式的使用详解原创 - CSDN博客
  13. JavaScript RegExp 对象的3 个方法:test()、exec() 和compile() 原创 - CSDN博客
  14. JavaScript RegExp test() 方法 - w3school 在线教程
  15. RegExp - JavaScript教程- 廖雪峰的官方网站
  16. Javascript中与正则表达式有关的方法(函数) - 秋雨沥沥
  17. 正则表达式和字符串的方法 - 现代JavaScript 教程
  18. RegExp 对象- JavaScript 教程- 网道
  19. JavaScript string的match和matchAll基本使用方法原创 - CSDN博客
  20. 【JS】match - search、replace、split、exec、test对比总结 - CV肉饼王
  21. 【正则】——深入正则表达式,手写常用正则表单验证 - 博客园
  22. 工作便利贴---正则匹配用户名&密码&邮箱&手机- Alive_2020 - 博客园
  23. 正则表达式高级用法(分组与捕获) - 五维思考 - 博客园
  24. JavaScript RegExp 对象的三种方法- 司徒骏 - 博客园
  25. 如何优化正则表达式的性能_日志服务(SLS) - 阿里云文档
  26. Java中的正则表达式优化:如何提高复杂文本匹配的性能 - CSDN博客
  27. 正则表达式——7种免费测试工具翻译 - CSDN博客
  28. 正则表达式在线测试网站推荐 - 稀土掘金
  29. js如何用字符串生成正则表达式 - PingCode
  30. 模式(Patterns)和修饰符(flags) - 现代JavaScript 教程
  31. 提升正则读写效率,超好用的正则图解工具Regulex与在线调试工具regexr - 听风是风 - 博客园