JavaScript 正则表达式:从零开始的实战对比
你是否也曾对正则表达式望而却步,觉得它的语法复杂难懂?今天,我们就用真实的代码对比来揭开它的神秘面纱。正则表达式,是编程语言中处理文本的强大工具,而 JavaScript 作为前端开发的主要语言,其正则表达式的应用更是广泛。我们不仅会探讨核心语法,还会通过具体的示例来对比不同的正则表达式方案,帮助你理解选择不同方案的实际影响。
核心语法
字符类
在正则表达式中,字符类用于匹配一组特定的字符。比如,用 [abc] 可以匹配单个字符 a、b 或 c。这个语法看似简单,但你是否想过 [abc] 和 [a-c] 之间的性能差异?
javascript
const text = "This is a sample text with lots of letters a and b and c";
const regex1 = /[abc]/g;
const regex2 = /[a-c]/g;
console.time("regex1");
const matches1 = text.match(regex1);
console.timeEnd("regex1");
console.time("regex2");
const matches2 = text.match(regex2);
console.timeEnd("regex2");
console.log(matches1); // ["a", "b", "c", "a", "b", "c"]
console.log(matches2); // ["a", "b", "c", "a", "b", "c"]
在这段代码中,regex1 和 regex2 都匹配了 a、b 和 c,但 [a-c] 的性能要优于 [abc]。这是因为 [a-c] 是一个范围匹配,JavaScript 引擎可以更高效地处理这种形式。
量词
量词用于指定某个元素可以出现的次数。常见的量词有 *(0 次或多次)、+(1 次或多次)、?(0 次或 1 次)。但你是否知道,这些量词在不同场景下的性能表现呢?
javascript
const text = "aaaaabaaaaa";
const regex1 = /a*/g;
const regex2 = /a+/g;
console.time("regex1");
const matches1 = text.match(regex1);
console.timeEnd("regex1");
console.time("regex2");
const matches2 = text.match(regex2);
console.timeEnd("regex2");
console.log(matches1); // ["aaaaa", "", "", "aaaaa", ""]
console.log(matches2); // ["aaaaa", "aaaaa"]
这里,regex1 使用了 * 量词,它会匹配 0 次或多次的 a,所以结果中包含了一些空字符串。而 regex2 使用了 + 量词,匹配 1 次或多次的 a,结果中没有空字符串。性能上,a* 因为需要处理空字符串的匹配,可能会比 a+ 稍微慢一些。
捕获组与非捕获组
捕获组(())和非捕获组((?:))用于分组,但前者会保存匹配结果,后者则不会。这看似微小的差别,却可能在处理大量数据时产生显著影响。
javascript
const text = "This is a test (123) (456) (789)";
const regex1 = /\((\d+)\)/g;
const regex2 = /\((?:\d+)\)/g;
console.time("regex1");
const matches1 = text.match(regex1);
console.timeEnd("regex1");
console.time("regex2");
const matches2 = text.match(regex2);
console.timeEnd("regex2");
console.log(matches1); // ["(123)", "(456)", "(789)"]
console.log(matches2); // ["(123)", "(456)", "(789)"]
在上面的代码中,regex1 使用了捕获组,而 regex2 使用了非捕获组。虽然匹配结果相同,但 regex2 的性能更好,因为它不需要保存每个捕获组的匹配结果。
前向肯定/否定及后向肯定/否定
前向肯定((?=...))和否定((?!...))以及后向肯定((?<=...))和否定((?<!...))是正则表达式中用来检查某个位置前后是否符合特定条件的语法。这些语法在某些场景下可以显著提高匹配效率。
javascript
const text = "This is a test 123 test 456 test 789";
const regex1 = /test (?=\d{3})/g;
const regex2 = /test \d{3}/g;
console.time("regex1");
const matches1 = text.match(regex1);
console.timeEnd("regex1");
console.time("regex2");
const matches2 = text.match(regex2);
console.timeEnd("regex2");
console.log(matches1); // ["test ", "test ", "test "]
console.log(matches2); // ["test 123", "test 456", "test 789"]
比较 regex1 和 regex2,regex1 使用了前向肯定,只匹配 test 后跟 3 个数字的位置,但不包含这些数字。而 regex2 匹配了 test 和其后的 3 个数字。前向肯定可以避免不必要的捕获,从而提高性能。
多行模式与全局模式
多行模式(m)和全局模式(g)是正则表达式中常用的修饰符。多行模式可以让你在多行文本中使用 ^ 和 $ 匹配行的开始和结束,而全局模式则会在整个字符串中查找所有匹配项。
javascript
const text = "Line1\nLine2\nLine3";
const regex1 = /^Line\d$/m;
const regex2 = /^Line\d$/;
console.time("regex1");
const matches1 = text.match(regex1);
console.timeEnd("regex1");
console.time("regex2");
const matches2 = text.match(regex2);
console.timeEnd("regex2");
console.log(matches1); // ["Line1", "Line2", "Line3"]
console.log(matches2); // null
这段代码中,regex1 使用了多行模式,可以匹配每行的开始和结束。而 regex2 没有使用多行模式,只匹配整个字符串的开始和结束,因此没有匹配到任何内容。多行模式在处理多行文本时非常有用,但需要根据实际需求选择是否使用。
懒惰匹配与贪婪匹配
懒惰匹配(*?, +?, ??)和贪婪匹配(*, +, ?)是正则表达式中用于控制匹配长度的量词。贪婪匹配会尽可能多地匹配,而懒惰匹配则尽可能少地匹配。
javascript
const text = "This is a test 123 test 456 test 789";
const regex1 = /test \d+/g;
const regex2 = /test \d+?/g;
console.time("regex1");
const matches1 = text.match(regex1);
console.timeEnd("regex1");
console.time("regex2");
const matches2 = text.match(regex2);
console.timeEnd("regex2");
console.log(matches1); // ["test 123", "test 456", "test 789"]
console.log(matches2); // ["test 1", "test 2", "test 3", "test 4", "test 5", "test 6", "test 7", "test 8", "test 9"]
regex1 使用了贪婪匹配,匹配了 test 后的所有数字。而 regex2 使用了懒惰匹配,只匹配了 test 后的第一个数字。懒惰匹配在某些情况下可以避免不必要的匹配,提高性能。
替换与分裂
正则表达式不仅用于匹配,还可以用于替换和分裂。在处理文本时,这两种操作非常常见,但也需要注意性能。
javascript
const text = "This is a test 123 test 456 test 789";
const regex1 = /test \d+/g;
const regex2 = /test \d+/;
console.time("replace");
const replaced = text.replace(regex1, "REPLACED");
console.timeEnd("replace");
console.time("split");
const split = text.split(regex2);
console.timeEnd("split");
console.log(replaced); // "This is a REPLACED REPLACED REPLACED"
console.log(split); // ["This is a ", " test ", " test ", " test ", "789"]
这里,replace 使用了全局模式,替换了所有匹配项。而 split 没有使用全局模式,只分裂了一次。全局模式在需要替换所有匹配项时非常有用,但在分裂操作中,一次匹配就足够了。
预编译
预编译是提高正则表达式性能的一个重要技巧。通过将正则表达式编译为一个正则对象,可以避免每次调用时的重复编译。
javascript
const text = "This is a test 123 test 456 test 789";
console.time("no-precompile");
const matches1 = text.match(/test \d+/g);
console.timeEnd("no-precompile");
console.time("precompile");
const regex = /test \d+/g;
const matches2 = text.match(regex);
console.timeEnd("precompile");
console.log(matches1); // ["test 123", "test 456", "test 789"]
console.log(matches2); // ["test 123", "test 456", "test 789"]
在上面的代码中,no-precompile 直接在 match 函数中传入正则表达式字符串,每次调用都会编译一次。而 precompile 先将正则表达式编译为一个对象,再进行匹配。预编译可以显著提高性能,尤其是在多次使用同一个正则表达式时。
Unicode 与非 Unicode
正则表达式中可以使用 Unicode 修饰符(u)来处理 Unicode 字符。这对于处理国际化文本非常重要,但性能上可能会稍有影响。
javascript
const text = "This is a test 😊";
const regex1 = /test \p{Emoji}/gu;
const regex2 = /test .+/g;
console.time("unicode");
const matches1 = text.match(regex1);
console.timeEnd("unicode");
console.time("non-unicode");
const matches2 = text.match(regex2);
console.timeEnd("non-unicode");
console.log(matches1); // ["test 😊"]
console.log(matches2); // ["test 😊"]
regex1 使用了 Unicode 修饰符,可以匹配 Unicode 表情符号。而 regex2 没有使用 Unicode 修饰符,匹配了 test 后的所有字符。Unicode 修饰符在处理国际化文本时是必须的,但会带来一定的性能开销。
性能测试工具
PERFORMANCE TIPS 为了更准确地测试正则表达式的性能,你可以使用 performance.now() 来获取高精度的时间戳。
javascript
const text = "This is a test 123 test 456 test 789";
const regex1 = /test \d+/g;
const regex2 = /test \d+/;
const start1 = performance.now();
const matches1 = text.match(regex1);
const end1 = performance.now();
const start2 = performance.now();
const matches2 = text.match(regex2);
const end2 = performance.now();
console.log(`Time for match with global flag: ${end1 - start1} ms`);
console.log(`Time for match without global flag: ${end2 - start2} ms`);
console.log(matches1); // ["test 123", "test 456", "test 789"]
console.log(matches2); // ["test 123"]
这个测试工具可以帮助你更准确地评估不同正则表达式方案的性能差异。
实战示例:提取电子邮件地址
假设你需要从一段文本中提取所有的电子邮件地址。你会如何选择正则表达式?
javascript
const text = "Contact us at support@example.com or feedback@example.org for more information.";
console.time("simple");
const matches1 = text.match(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,4}\b/g);
console.timeEnd("simple");
console.time("complex");
const matches2 = text.match(/\b[A-Za-z0-9._%+-]+(?:@[A-Za-z0-9.-]+\.[A-Z]{2,4})\b/g);
console.timeEnd("complex");
console.log(`Time for simple regex: ${matches1.time} ms`);
console.log(`Time for complex regex: ${matches2.time} ms`);
console.log(matches1); // ["support@example.com", "feedback@example.org"]
console.log(matches2); // ["support@example.com", "feedback@example.org"]
simple 正则表达式直接匹配了电子邮件地址,而 complex 正则表达式使用了非捕获组。虽然两者的结果相同,但 complex 方案在处理大量数据时可能会更高效,因为它不需要保存捕获组的结果。
实战示例:验证密码强度
假设你需要验证一个密码是否符合特定的强度要求,比如包含至少 8 个字符,包含大写字母、小写字母和数字。
javascript
const password = "Abc12345";
const regex1 = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,}$/;
const regex2 = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,20}$/;
console.time("regex1");
const valid1 = regex1.test(password);
console.timeEnd("regex1");
console.time("regex2");
const valid2 = regex2.test(password);
console.timeEnd("regex2");
console.log(`Time for regex1: ${matches1.time} ms`);
console.log(`Time for regex2: ${matches2.time} ms`);
console.log(valid1); // true
console.log(valid2); // true
regex1 没有指定密码的最大长度,而 regex2 指定了密码的最大长度为 20 个字符。在实际应用中,regex2 可以防止过长的密码导致性能问题。
性能优化技巧
- 避免不必要的捕获组 :使用非捕获组
(?:)可以提高性能。 - 使用适当的量词 :选择适合场景的量词,比如在匹配数字时使用
\d+而不是\d*。 - 预编译正则表达式:将正则表达式编译为对象,避免每次调用时的重复编译。
- 使用多行模式 :处理多行文本时,使用多行模式
m。 - 使用 Unicode 修饰符 :处理国际化文本时,使用 Unicode 修饰符
u。 - 限制匹配长度 :使用
{min,max}量词限制匹配的长度,防止过长的匹配导致性能问题。
实用工具推荐
在学习和使用正则表达式的过程中,一些实用工具可以帮助你更高效地编写和测试正则表达式。Hey Cron 是一个多功能的在线工具网站,它不仅提供了正则表达式生成器,还有很多其他实用工具,比如:
- 正则表达式生成器:输入文本描述,自动生成正则表达式。
- Cron 表达式生成器:输入中文描述,自动生成 Cron 表达式。
- 中英互译:快速翻译中英文文本。
- JSON 格式化:帮助你快速格式化和验证 JSON 数据。
- Base64 编码解码:轻松进行 Base64 编码和解码。
- 时间戳转换:将时间戳转换为各种日期格式。
- JWT 解析:解析 JWT 令牌,查看其内容。
这些工具不仅可以提高你的开发效率,还能帮助你在实际项目中更好地应用正则表达式。不妨试试 Hey Cron,让你的开发之路更加顺畅。