前端正则表达式完全指南
正则表达式(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]
-
单个字符匹配:
-
量词(Quantifiers) : 用于指定前面一个字符或子表达式出现的次数。 [3]
-
锚点(Anchors) : 用于指定匹配发生的位置。
-
字符类(Character Classes) :
-
分组与捕获(Grouping and Capturing) :
-
或操作符:
x|y: 匹配x或y。 [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):const str = "hello world"; const regex = /hello/; console.log(regex.test(str)); // 输出: true const regexNoMatch = /javascript/; console.log(regexNoMatch.test(str)); // 输出: false -
exec(string):-
如果匹配成功,返回一个数组,其中第一个元素是整个匹配的字符串,后续元素是捕获组匹配的子串。该数组还有额外的属性:
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]
- 如果正则表达式没有全局标志
g,match()的行为与RegExp.prototype.exec()类似,返回第一个匹配及其捕获组,失败则返回null。 [18] - 如果正则表达式设置了全局标志
g,match()会返回一个包含所有匹配子串的数组(不包含捕获组信息),如果没有任何匹配,则返回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):-
返回一个新的字符串,原始字符串不变。
-
如果第一个参数是正则表达式且设置了
g标志,则替换所有匹配项;否则只替换第一个匹配项。 -
第二个参数可以是字符串,也可以是一个函数。
-
字符串替换: 可以使用特殊替换模式:
$$: 插入一个$字符。$&: 插入匹配的子串。- `$``: 插入当前匹配左边的内容。
$': 插入当前匹配右边的内容。$n: 插入第n个捕获组的内容(如果存在)。n是从1开始的数字。$<Name>: 插入名为Name的捕获组的内容(如果存在)。
-
函数替换: 该函数会在每次匹配时被调用,其返回值将作为替换字符串。函数会接收多个参数:
match: 匹配的子串(同$&)。p1, p2, ...: 第1个、第2个...捕获组的内容(如果正则中有捕获组)。offset: 匹配的子串在原字符串中的偏移量(索引)。string: 被操作的原始字符串。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):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>
代码讲解:
-
HTML结构:
- 一个标准的HTML表单,包含用户名、邮箱、密码、确认密码、手机号、可选的个人网站和出生日期字段。
- 每个输入字段后面都有一个
div用于显示错误或成功消息。 - 使用了简单的CSS进行样式化,使其更易于查看。
-
JavaScript -
regexPatterns对象:-
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,}$/ -
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.-]+): 域名主体,例如example或sub.example。.: 域名和顶级域名之间的点。([a-z.]{2,6}): 顶级域名,例如com或co.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函数中,我们添加了额外的 JavaScriptDate对象检查来处理部分这类无效日期。
-
辅助函数 (
showError,showSuccess,clearMessage) :- 这些函数用于在对应的错误消息
div中显示反馈,并改变输入框的边框颜色。
- 这些函数用于在对应的错误消息
-
验证函数 (
validateUsername,validateEmail, etc.) :- 每个字段都有一个对应的验证函数。
- 首先获取输入值并
trim()(去除首尾空格,密码除外)。 - 检查值是否为空(如果字段是必需的)。
- 使用
regexPatterns中对应的正则表达式的test()方法来验证格式。 [14] - 根据验证结果调用
showError或showSuccess。 - 返回
true(有效) 或false(无效)。 validateBirthdate函数中包含了对日期逻辑有效性的额外检查,因为单纯的正则表达式难以完美覆盖所有日期规则(如不同月份的天数、闰年)。
-
事件监听器:
- 为每个输入字段添加
blur事件监听器,当用户离开输入框时触发验证。 - 密码字段和确认密码字段还添加了
input事件监听器,以提供更即时的反馈。 - 表单的
submit事件被拦截 (event.preventDefault())。 - 在提交时,会再次执行所有验证函数。
- 如果所有字段都有效,则显示成功消息,并可以执行实际的提交操作(例如,通过
fetchAPI 将数据发送到服务器)。否则,提示用户检查错误。
- 为每个输入字段添加
-
正则表达式解释:
- 在
<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]
推荐好文:
- 正则表达式- JavaScript - MDN Web Docs
- 一文详解JavaScript中的正则表达式 - 稀土掘金
- 前端正则最全知识汇总(学会正则收藏它就够啦)
- 前端必经之路:带你读懂正则表达式 - CSDN博客
- js如何生成动态正则表达式? 原创 - CSDN博客
- 前端-JS正则表达式快速入门 - 稀土掘金
- 【正则表达式系列】一些概念(字符组、捕获组、非捕获组) - Dailc的个人主页
- 用正则表达式分析URL - Harttle Land
- 正则表达式详解及实战 - 稀土掘金
- 正则表达式高级用法 - 阿里云开发者社区
- 正则表达式前端使用手册 - louis blog
- JavaScript正则表达式的使用详解原创 - CSDN博客
- JavaScript RegExp 对象的3 个方法:test()、exec() 和compile() 原创 - CSDN博客
- JavaScript RegExp test() 方法 - w3school 在线教程
- RegExp - JavaScript教程- 廖雪峰的官方网站
- Javascript中与正则表达式有关的方法(函数) - 秋雨沥沥
- 正则表达式和字符串的方法 - 现代JavaScript 教程
- RegExp 对象- JavaScript 教程- 网道
- JavaScript string的match和matchAll基本使用方法原创 - CSDN博客
- 【JS】match - search、replace、split、exec、test对比总结 - CV肉饼王
- 【正则】——深入正则表达式,手写常用正则表单验证 - 博客园
- 工作便利贴---正则匹配用户名&密码&邮箱&手机- Alive_2020 - 博客园
- 正则表达式高级用法(分组与捕获) - 五维思考 - 博客园
- JavaScript RegExp 对象的三种方法- 司徒骏 - 博客园
- 如何优化正则表达式的性能_日志服务(SLS) - 阿里云文档
- Java中的正则表达式优化:如何提高复杂文本匹配的性能 - CSDN博客
- 正则表达式——7种免费测试工具翻译 - CSDN博客
- 正则表达式在线测试网站推荐 - 稀土掘金
- js如何用字符串生成正则表达式 - PingCode
- 模式(Patterns)和修饰符(flags) - 现代JavaScript 教程
- 提升正则读写效率,超好用的正则图解工具Regulex与在线调试工具regexr - 听风是风 - 博客园