探索使用正则表达式进行高级 JavaScript 模式匹配。学习 regex 语法、实际应用和优化技术,以编写高效、健壮的代码。
JavaScript 正则表达式模式匹配:综合指南
正则表达式 (regex) 是 JavaScript 中用于模式匹配和文本操作的强大工具。它们允许开发人员根据定义的模式搜索、验证和转换字符串。本指南全面概述了 JavaScript 中的正则表达式,涵盖了语法、用法和高级技术。
什么是正则表达式?
正则表达式是定义搜索模式的字符序列。这些模式用于匹配和操作字符串。正则表达式在编程中广泛用于以下任务:
- 数据验证:确保用户输入符合特定格式(例如,电子邮件地址、电话号码)。
- 数据提取:从文本中检索特定信息(例如,提取日期、URL 或价格)。
- 搜索和替换:根据复杂模式查找和替换文本。
- 文本处理:根据定义的规则拆分、连接或转换字符串。
在 JavaScript 中创建正则表达式
在 JavaScript 中,可以通过两种方式创建正则表达式:
- 使用正则表达式字面量:将模式包含在正斜杠 (
/) 中。 - 使用
RegExp构造函数:使用模式字符串创建一个RegExp对象。
示例:
// 使用正则表达式字面量
const regexLiteral = /hello/;
// 使用 RegExp 构造函数
const regexConstructor = new RegExp("hello");
两种方法之间的选择取决于模式是在编译时已知还是动态生成。当模式是固定的且预先知道时,请使用字面量表示法。当模式需要以编程方式构建时,尤其是在包含变量时,请使用构造函数。
基本 Regex 语法
正则表达式由表示要匹配的模式的字符组成。以下是一些基本的 regex 组件:
- 字面量字符:匹配字符本身(例如,
/a/匹配字符 'a')。 - 元字符:具有特殊含义(例如,
.,^,$,*,+,?,[],{},(),\,|)。 - 字符类:表示一组字符(例如,
[abc]匹配 'a'、'b' 或 'c')。 - 量词:指定一个字符或组应该出现的次数(例如,
*,+,?,{n},{n,},{n,m})。 - 锚点:匹配字符串中的位置(例如,
^匹配开头,$匹配结尾)。
常见元字符:
.(点号): 匹配除换行符外的任何单个字符。^(插入符号): 匹配字符串的开头。$(美元符号): 匹配字符串的结尾。*(星号): 匹配前一个字符或组的零次或多次出现。+(加号): 匹配前一个字符或组的一次或多次出现。?(问号): 匹配前一个字符或组的零次或一次出现。 用于可选字符。[](方括号): 定义一个字符类,匹配括号内的任何单个字符。{}(花括号): 指定要匹配的出现次数。{n}匹配正好 n 次,{n,}匹配 n 次或更多次,{n,m}匹配 n 到 m 次之间。()(圆括号): 将字符组合在一起并捕获匹配的子字符串。\(反斜杠): 转义元字符,允许您按字面意思匹配它们。|(管道符): 作为“或”运算符,匹配其前面或后面的表达式。
字符类:
[abc]: 匹配 a、b 或 c 中的任何一个字符。[^abc]: 匹配任何*不是* a、b 或 c 的字符。[a-z]: 匹配从 a 到 z 的任何小写字母。[A-Z]: 匹配从 A 到 Z 的任何大写字母。[0-9]: 匹配从 0 到 9 的任何数字。[a-zA-Z0-9]: 匹配任何字母数字字符。\d: 匹配任何数字 (等效于[0-9])。\D: 匹配任何非数字字符 (等效于[^0-9])。\w: 匹配任何单词字符(字母数字加下划线;等效于[a-zA-Z0-9_])。\W: 匹配任何非单词字符 (等效于[^a-zA-Z0-9_])。\s: 匹配任何空白字符(空格、制表符、换行符等)。\S: 匹配任何非空白字符。
量词:
*: 匹配前一个元素零次或多次。例如,a*匹配 "", "a", "aa", "aaa" 等。+: 匹配前一个元素一次或多次。例如,a+匹配 "a", "aa", "aaa",但不匹配 ""。?: 匹配前一个元素零次或一次。例如,a?匹配 "" 或 "a"。{n}: 匹配前一个元素正好 *n* 次。例如,a{3}匹配 "aaa"。{n,}: 匹配前一个元素 *n* 次或更多次。例如,a{2,}匹配 "aa", "aaa", "aaaa" 等。{n,m}: 匹配前一个元素在 *n* 和 *m* 次之间(含)。例如,a{2,4}匹配 "aa", "aaa" 或 "aaaa"。
锚点:
^: 匹配字符串的开头。例如,^Hello匹配以 "Hello" *开头*的字符串。$: 匹配字符串的结尾。例如,World$匹配以 "World" *结尾*的字符串。\b: 匹配单词边界。这是单词字符 (\w) 和非单词字符 (\W) 之间或字符串开头或结尾的位置。例如,\bword\b匹配整个单词 "word"。
标志:
Regex 标志修改正则表达式的行为。它们附加在 regex 字面量的末尾或作为第二个参数传递给 RegExp 构造函数。
g(global): 匹配模式的所有出现,而不仅仅是第一个。i(ignore case): 执行不区分大小写的匹配。m(multiline): 启用多行模式,其中^和$匹配每行的开头和结尾(由\n分隔)。s(dotAll): 允许点号 (.) 也匹配换行符。u(unicode): 启用完整的 Unicode 支持。y(sticky): 仅从 regex 的lastIndex属性指示的索引开始匹配。
JavaScript Regex 方法
JavaScript 提供了几种用于处理正则表达式的方法:
test(): 测试字符串是否匹配模式。返回true或false。exec(): 在字符串中执行搜索匹配。返回一个包含匹配文本和捕获组的数组,如果未找到匹配项,则返回null。match(): 返回一个包含字符串与正则表达式匹配结果的数组。在使用和不使用g标志时的行为不同。search(): 测试字符串中的匹配。返回第一个匹配的索引,如果未找到匹配项,则返回 -1。replace(): 用替换字符串或返回替换字符串的函数替换模式的出现。split(): 根据正则表达式将字符串拆分为子字符串数组。
使用 Regex 方法的示例:
// test()
const regex = /hello/;
const str = "hello world";
console.log(regex.test(str)); // 输出: true
// exec()
const regex2 = /hello (\w+)/;
const str2 = "hello world";
const result = regex2.exec(str2);
console.log(result); // 输出: ["hello world", "world", index: 0, input: "hello world", groups: undefined]
// match() 带 'g' 标志
const regex3 = /\d+/g; // 全局匹配一个或多个数字
const str3 = "There are 123 apples and 456 oranges.";
const matches = str3.match(regex3);
console.log(matches); // 输出: ["123", "456"]
// match() 不带 'g' 标志
const regex4 = /\d+/;
const str4 = "There are 123 apples and 456 oranges.";
const match = str4.match(regex4);
console.log(match); // 输出: ["123", index: 11, input: "There are 123 apples and 456 oranges.", groups: undefined]
// search()
const regex5 = /world/;
const str5 = "hello world";
console.log(str5.search(regex5)); // 输出: 6
// replace()
const regex6 = /world/;
const str6 = "hello world";
const newStr = str6.replace(regex6, "JavaScript");
console.log(newStr); // 输出: hello JavaScript
// replace() 使用函数
const regex7 = /(\d+)-(\d+)-(\d+)/;
const str7 = "Today's date is 2023-10-27";
const newStr2 = str7.replace(regex7, (match, year, month, day) => {
return `${day}/${month}/${year}`;
});
console.log(newStr2); // 输出: Today's date is 27/10/2023
// split()
const regex8 = /, /;
const str8 = "apple, banana, cherry";
const arr = str8.split(regex8);
console.log(arr); // 输出: ["apple", "banana", "cherry"]
高级 Regex 技术
捕获组:
圆括号 () 用于在正则表达式中创建捕获组。捕获组允许您提取匹配文本的特定部分。exec() 和 match() 方法返回一个数组,其中第一个元素是整个匹配项,后续元素是捕获的组。
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const dateString = "2023-10-27";
const match = regex.exec(dateString);
console.log(match[0]); // 输出: 2023-10-27 (整个匹配项)
console.log(match[1]); // 输出: 2023 (第一个捕获组 - 年)
console.log(match[2]); // 输出: 10 (第二个捕获组 - 月)
console.log(match[3]); // 输出: 27 (第三个捕获组 - 日)
命名捕获组:
ES2018 引入了命名捕获组,允许您使用语法 (? 为捕获组分配名称。这使得代码更具可读性和可维护性。
const regex = /(?\d{4})-(?\d{2})-(?\d{2})/;
const dateString = "2023-10-27";
const match = regex.exec(dateString);
console.log(match.groups.year); // 输出: 2023
console.log(match.groups.month); // 输出: 10
console.log(match.groups.day); // 输出: 27
非捕获组:
如果您需要对 regex 的某些部分进行分组而不捕获它们(例如,将量词应用于一个组),您可以使用非捕获组,语法为 (?:...)。这可以避免为捕获组分配不必要的内存。
const regex = /(?:https?:\/\/)?([\w\.]+)/; // 匹配 URL,但只捕获域名
const url = "https://www.example.com/path";
const match = regex.exec(url);
console.log(match[1]); // 输出: www.example.com
环视 (Lookarounds):
环视是零宽度断言,它根据某个位置之前(后行断言)或之后(先行断言)的模式来匹配该位置,而不会将环视模式本身包含在匹配结果中。
- 正向先行断言 (Positive Lookahead):
(?=...)如果先行断言内的模式*跟随*在当前位置之后,则匹配。 - 负向先行断言 (Negative Lookahead):
(?!...)如果先行断言内的模式*不*跟随在当前位置之后,则匹配。 - 正向后行断言 (Positive Lookbehind):
(?<=...)如果后行断言内的模式*位于*当前位置之前,则匹配。 - 负向后行断言 (Negative Lookbehind):
(? 如果后行断言内的模式*不*位于当前位置之前,则匹配。
示例:
// 正向先行断言:仅当后面是 USD 时才获取价格
const regex = /\d+(?= USD)/;
const text = "The price is 100 USD";
const match = text.match(regex);
console.log(match); // 输出: ["100"]
// 负向先行断言:仅当后面不是数字时才获取单词
const regex2 = /\b\w+\b(?! \d)/;
const text2 = "apple 123 banana orange 456";
const matches = text2.match(regex2);
console.log(matches); // 输出: null,因为 match() 在没有 'g' 标志的情况下只返回第一个匹配,这不是我们需要的。
// 修正方法:
const regex3 = /\b\w+\b(?! \d)/g;
const text3 = "apple 123 banana orange 456";
const matches3 = text3.match(regex3);
console.log(matches3); // 输出: [ 'banana' ]
// 正向后行断言:仅当以 $ 开头时才获取值
const regex4 = /(?<=L\$)\d+/;
const text4 = "The price is $200";
const match4 = text4.match(regex4);
console.log(match4); // 输出: ["200"]
// 负向后行断言:仅当不以 'not' 单词开头时才获取单词
const regex5 = /(?
反向引用:
反向引用允许您在同一个正则表达式中引用先前捕获的组。它们使用语法 \1, \2 等,其中数字对应于捕获组的编号。
const regex = /([a-z]+) \1/;
const text = "hello hello world";
const match = regex.exec(text);
console.log(match); // 输出: ["hello hello", "hello", index: 0, input: "hello hello world", groups: undefined]
正则表达式的实际应用
验证电子邮件地址:
正则表达式的一个常见用例是验证电子邮件地址。虽然一个完美的电子邮件验证 regex 极其复杂,但这里有一个简化的示例:
const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
console.log(emailRegex.test("test@example.com")); // 输出: true
console.log(emailRegex.test("invalid-email")); // 输出: false
console.log(emailRegex.test("test@sub.example.co.uk")); // 输出: true
从文本中提取 URL:
您可以使用正则表达式从一段文本中提取 URL:
const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
const text = "Visit our website at https://www.example.com or check out http://blog.example.org.";
const urls = text.match(urlRegex);
console.log(urls); // 输出: ["https://www.example.com", "http://blog.example.org"]
解析 CSV 数据:
正则表达式可用于解析 CSV(逗号分隔值)数据。以下是一个将 CSV 字符串拆分为值数组的示例,该示例处理带引号的字段:
const csvString = 'John,Doe,"123, Main St",New York';
const csvRegex = /(?:"([^"]*(?:""[^"]*)*)")|([^,]+)/g; // 修正后的 CSV regex
let values = [];
let match;
while (match = csvRegex.exec(csvString)) {
values.push(match[1] ? match[1].replace(/""/g, '"') : match[2]);
}
console.log(values); // 输出: ["John", "Doe", "123, Main St", "New York"]
国际电话号码验证
验证国际电话号码很复杂,因为格式和长度各不相同。一个健壮的解决方案通常需要使用库,但一个简化的 regex 可以提供基本的验证:
const phoneRegex = /^\+(?:[0-9] ?){6,14}[0-9]$/;
console.log(phoneRegex.test("+1 555 123 4567")); // 输出: true (美国示例)
console.log(phoneRegex.test("+44 20 7946 0500")); // 输出: true (英国示例)
console.log(phoneRegex.test("+81 3 3224 5000")); // 输出: true (日本示例)
console.log(phoneRegex.test("123-456-7890")); // 输出: false
密码强度验证
正则表达式对于强制执行密码强度策略很有用。下面的示例检查最小长度、大写、小写和数字。
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
console.log(passwordRegex.test("P@ssword123")); // 输出: true
console.log(passwordRegex.test("password")); // 输出: false (没有大写字母或数字)
console.log(passwordRegex.test("Password")); // 输出: false (没有数字)
console.log(passwordRegex.test("Pass123")); // 输出: false (没有小写字母)
console.log(passwordRegex.test("P@ss1")); // 输出: false (少于 8 个字符)
Regex 优化技术
正则表达式的计算成本可能很高,特别是对于复杂的模式或大量的输入。以下是一些优化 regex 性能的技术:
- 具体化:避免使用过于通用的模式,这可能会匹配超出预期的内容。
- 使用锚点:尽可能将 regex 锚定到字符串的开头或结尾 (
^,$)。 - 避免回溯:通过使用所有格量词(例如,用
++代替+)或原子组 ((?>...)) 来最小化回溯。 - 编译一次:如果您多次使用同一个 regex,请编译一次并重用
RegExp对象。 - 明智地使用字符类:字符类 (
[]) 通常比交替 (|) 更快。 - 保持简单:避免过于复杂、难以理解和维护的 regex。有时,将复杂的任务分解为多个更简单的 regex 或使用其他字符串操作技术可能更有效。
常见的 Regex 错误
- 忘记转义元字符:当您想按字面意思匹配特殊字符(如
.,*,+,?,$,^,(,),[,],{,},|, 和\)时,未能转义它们。 - 过度使用
.(点号):点号匹配任何字符(在某些模式下除换行符外),如果不小心使用,可能会导致意外匹配。尽可能使用字符类或其他更具限制性的模式来使其更具体。 - 贪婪性:默认情况下,像
*和+这样的量词是贪婪的,会尽可能多地匹配。当您需要匹配尽可能短的字符串时,请使用惰性量词 (*?,+?)。 - 不正确地使用锚点:误解
^(字符串/行的开头)和$(字符串/行的结尾)的行为会导致不正确的匹配。在处理多行字符串并希望^和$匹配每行的开头和结尾时,请记住使用m(多行)标志。 - 未处理边缘情况:未能考虑所有可能的输入场景和边缘情况会导致错误。使用各种输入(包括空字符串、无效字符和边界条件)彻底测试您的 regex。
- 性能问题:构建过于复杂和低效的 regex 会导致性能问题,尤其是在处理大量输入时。通过使用更具体的模式、避免不必要的回溯以及编译重复使用的 regex 来优化您的 regex。
- 忽略字符编码:未正确处理字符编码(尤其是 Unicode)会导致意外结果。在处理 Unicode 字符时,请使用
u标志以确保正确匹配。
结论
正则表达式是 JavaScript 中用于模式匹配和文本操作的宝贵工具。掌握 regex 语法和技术可以使您高效地解决从数据验证到复杂文本处理的各种问题。通过理解本指南中讨论的概念并结合实际示例进行练习,您可以熟练使用正则表达式来提升您的 JavaScript 开发技能。
请记住,正则表达式可能很复杂,使用在线 regex 测试工具(如 regex101.com 或 regexr.com)进行彻底测试通常很有帮助。这使您可以可视化匹配项并有效地调试任何问题。编程愉快!