引言
在前端面试圈,正则表达式是个 "分水岭" 考点 ------ 基础不牢的人靠零散记忆拼凑答案,而吃透核心的人能结合场景灵活拆解,甚至反向优化性能。很多开发者觉得正则 "难记易混",其实是没抓住 "从基础规则到面试场景" 的逻辑链。这篇文章会从正则底层概念讲到前端高频面试题,每个知识点都配面试易错点和实战代码,帮你一篇吃透正则,下次面试遇到正则问题,直接从 "卡壳" 变 "侃侃而谈"。
正则表达式基础
创建正则表达式
在JavaScript中,创建正则表达式主要有两种方式:字面量方式和构造函数方式。
1. 字面量方式
字面量方式是最常用也最简洁的创建正则表达式的方法。它使用斜杠 / 将正则表达式模式包围起来,后面可以跟零个或多个标志(flags)。
javascript
// 匹配字符串中的 "hello"
const regex1 = /hello/;
// 匹配字符串中的 "hello",不区分大小写 (i 标志)
const regex2 = /hello/i;
// 匹配字符串中的所有 "hello" (g 标志)
const regex3 = /hello/g;
// 匹配字符串中的所有 "hello",不区分大小写 (i 和 g 标志)
const regex4 = /hello/gi;
优点:
- 语法简洁,易于阅读和编写。
- 在脚本加载时编译,性能更优,适合模式固定不变的场景。
缺点:
- 模式是硬编码的,不能动态改变。
2. 构造函数方式
构造函数方式通过 new RegExp() 来创建正则表达式对象。这种方式允许你使用字符串来定义正则表达式模式,因此可以在运行时动态地构建正则表达式。
javascript
// 匹配字符串中的 "world"
const pattern1 = "world";
const regex5 = new RegExp(pattern1);
// 匹配字符串中的 "world",不区分大小写
const pattern2 = "world";
const flags2 = "i";
const regex6 = new RegExp(pattern2, flags2);
// 匹配字符串中的所有 "world"
const pattern3 = "world";
const flags3 = "g";
const regex7 = new RegExp(pattern3, flags3);
// 匹配字符串中的所有 "world",不区分大小写
const pattern4 = "world";
const flags4 = "gi";
const regex8 = new RegExp(pattern4, flags4);
注意事项:
- 如果模式字符串中包含特殊字符(如
\),需要进行双重转义,因为字符串本身会先进行一次转义解析。 例如,要匹配一个点.,在字面量中是/\./,在构造函数中则是new RegExp("\\.")。
优点:
- 模式可以动态生成,适用于模式不确定或需要根据用户输入等情况动态变化的场景。
缺点:
- 语法相对复杂,需要注意字符串转义。
- 在运行时编译,性能可能略低于字面量方式。
基本匹配
正则表达式的核心在于匹配字符串中的模式。最简单的匹配就是普通字符的匹配。
1. 普通字符匹配
大多数字符在正则表达式中都表示它们自身,即它们会匹配文本中完全相同的字符。例如,正则表达式 /abc/ 会精确匹配字符串中的 "abc"。
javascript
const str = "hello abc world";
const regex = /abc/;
console.log(regex.test(str)); // true
console.log(str.match(regex)); // ["abc", index: 6, input: "hello abc world", groups: undefined]
2. 特殊字符转义 (\)
正则表达式中有很多具有特殊含义的字符,它们被称为"元字符"(后面会详细介绍)。如果我们需要匹配这些特殊字符本身,而不是它们所代表的特殊含义,就需要使用反斜杠 \ 进行转义。
常见的需要转义的特殊字符包括:., *, +, ?, ^, $, (, ), [, ], {, }, |, \, /。
示例:
-
匹配一个点
.:javascriptconst str1 = "file.txt"; const regex1 = /\./; // 转义点号,匹配字面量点号 console.log(regex1.test(str1)); // true -
匹配一个星号
*:javascriptconst str2 = "price*quantity"; const regex2 = /\*/; // 转义星号,匹配字面量星号 console.log(regex2.test(str2)); // true -
匹配一个反斜杠
\:javascriptconst str3 = "C:\\Users\\Desktop"; const regex3 = /\\/; // 转义反斜杠,匹配字面量反斜杠 console.log(regex3.test(str3)); // true
元字符与字符类
元字符是正则表达式中具有特殊含义的字符,它们不代表字符本身,而是代表一类字符或某种位置。字符类则允许我们匹配一组字符中的任意一个。
. (点号)
匹配除换行符(\n、\r)之外的任何单个字符。如果设置了 s (dotAll) 标志,则 . 也能匹配换行符。
示例:
javascript
/a.c/.test("abc"); // true
/a.c/.test("adc"); // true
/a.c/.test("a\nc"); // false (默认情况下不匹配换行符)
/a.c/s.test("a\nc"); // true (s 标志使其匹配换行符)
\d, \D (数字与非数字)
\d: 匹配任何一个数字字符,等价于[0-9]。\D: 匹配任何一个非数字字符,等价于[^0-9]。
示例:
javascript
/\d/.test("123"); // true
/\D/.test("abc"); // true
/\d{3}/.test("123"); // true (匹配连续三个数字)
\w, \W (字母、数字、下划线与非)
\w: 匹配任何一个字母、数字或下划线字符,等价于[A-Za-z0-9_]。\W: 匹配任何一个非字母、数字或下划线字符,等价于[^A-Za-z0-9_]。
示例:
javascript
/\w/.test("a"); // true
/\w/.test("1"); // true
/\w/.test("_"); // true
/\W/.test("!"); // true
\s, \S (空白字符与非)
\s: 匹配任何一个空白字符,包括空格、制表符(\t)、换页符(\f)、换行符(\n)、回车符(\r)和垂直制表符(\v)。\S: 匹配任何一个非空白字符。
示例:
javascript
/\s/.test(" "); // true
/\s/.test("\n"); // true
/\S/.test("a"); // true
[] (字符集合)
匹配方括号 [] 中所包含的任意一个字符。你可以列出单个字符,也可以使用连字符 - 来表示一个字符范围。
示例:
javascript
/[abc]/.test("a"); // true (匹配 'a' 或 'b' 或 'c')
/[0-9]/.test("5"); // true (匹配任何数字)
/[a-zA-Z]/.test("Z"); // true (匹配任何大小写字母)
/[aeiou]/.test("apple"); // true (匹配任何元音字母)
[^] (否定字符集合)
匹配不在方括号 [] 中所包含的任意一个字符。^ 在字符集合内部表示"非"或"不包含"。
示例:
javascript
/[^abc]/.test("d"); // true (匹配除 'a', 'b', 'c' 之外的任何字符)
/[^0-9]/.test("a"); // true (匹配任何非数字字符)
量词
量词用于指定一个字符、字符类或分组可以出现的次数。
* (零次或多次)
匹配前面的元素零次或多次。等价于 {0,}。
示例:
javascript
/a*b/.test("b"); // true (a 出现 0 次)
/a*b/.test("ab"); // true (a 出现 1 次)
/a*b/.test("aaab"); // true (a 出现 3 次)
+ (一次或多次)
匹配前面的元素一次或多次。等价于 {1,}。
示例:
javascript
/a+b/.test("b"); // false (a 出现 0 次)
/a+b/.test("ab"); // true (a 出现 1 次)
/a+b/.test("aaab"); // true (a 出现 3 次)
? (零次或一次)
匹配前面的元素零次或一次。等价于 {0,1}。它也可以用于使量词变为非贪婪模式(见下文)。
示例:
javascript
/a?b/.test("b"); // true (a 出现 0 次)
/a?b/.test("ab"); // true (a 出现 1 次)
/a?b/.test("aaab"); // false (a 出现多次)
{n} (恰好n次)
匹配前面的元素恰好 n 次。
示例:
javascript
/a{3}b/.test("aaab"); // true
/a{3}b/.test("ab"); // false
{n,} (至少n次)
匹配前面的元素至少 n 次。
示例:
javascript
/a{2,}b/.test("aab"); // true
/a{2,}b/.test("aaab"); // true
/a{2,}b/.test("ab"); // false
{n,m} (n到m次)
匹配前面的元素至少 n 次,但不超过 m 次。
示例:
javascript
/a{1,3}b/.test("ab"); // true
/a{1,3}b/.test("aab"); // true
/a{1,3}b/.test("aaab"); // true
/a{1,3}b/.test("aaaab"); // false
贪婪模式与非贪婪模式 (?)
正则表达式的量词默认是"贪婪"的,这意味着它们会尽可能多地匹配字符。例如,/a.*b/ 会匹配从第一个 a 到最后一个 b 之间的所有内容。
贪婪模式示例:
javascript
const str = "a text b and another a text b";
const regex = /a.*b/;
console.log(str.match(regex)); // ["a text b and another a text b"]
通过在量词后面添加 ?,可以使其变为"非贪婪"(或"惰性")模式,这意味着它们会尽可能少地匹配字符。
非贪婪模式示例:
javascript
const str = "a text b and another a text b";
const regex = /a.*?b/;
console.log(str.match(regex)); // ["a text b"]
边界匹配
边界匹配符用于匹配字符串中的特定位置,而不是特定的字符。
^ (开头)
匹配字符串的开头。如果设置了 m (多行) 标志,则它也匹配每行的开头。
示例:
javascript
/^hello/.test("hello world"); // true
/^world/.test("hello world"); // false
const multiLineStr = "first line\nsecond line";
/^second/.test(multiLineStr); // false (默认模式下只匹配整个字符串开头)
/^second/m.test(multiLineStr); // true (m 标志使其匹配每行开头)
$ (结尾)
匹配字符串的结尾。如果设置了 m (多行) 标志,则它也匹配每行的结尾。
示例:
javascript
/world$/.test("hello world"); // true
/hello$/.test("hello world"); // false
const multiLineStr = "first line\nsecond line";
/line$/.test(multiLineStr); // true (默认模式下匹配整个字符串结尾)
/line$/m.test(multiLineStr); // true (m 标志使其匹配每行结尾)
\b, \B (单词边界与非单词边界)
\b: 匹配一个单词边界。单词边界是指单词字符(\w)和非单词字符(\W)之间的位置,或者单词字符与字符串开头/结尾之间的位置。\B: 匹配一个非单词边界。即\b不匹配的位置。
示例:
javascript
/\bcat\b/.test("cat"); // true (匹配独立的单词 "cat")
/\bcat\b/.test("category"); // false ("cat" 后面不是单词边界)
/cat\b/.test("category"); // true ("cat" 后面是单词边界,因为 y 是非单词字符)
/\Bcat/.test("category"); // true ("cat" 前面是非单词边界,因为 c 是单词字符)
/\Bcat\B/.test("wildcatting"); // true (匹配 "wildcatting" 中的 "cat")
正则表达式进阶
分组与捕获
分组是正则表达式中一个非常强大的特性,它允许我们将多个字符视为一个独立的单元进行操作。分组主要通过圆括号 () 来实现,同时它也带来了"捕获"的功能。
() (分组与捕获)
圆括号 () 有两个主要作用:
-
分组: 将多个字符组合成一个逻辑单元,可以对这个单元应用量词。 示例:
javascript/(ab)+/.test("ababab"); // true (匹配一个或多个 "ab") /ab+/.test("ababab"); // true (匹配 "a" 后面跟一个或多个 "b") -
捕获: 捕获匹配到的子字符串,并将其存储起来,以便后续引用。每个捕获组都会被分配一个从1开始的数字,可以通过反向引用或
match()方法的返回结果来访问。 示例:javascriptconst str = "foo bar baz"; const regex = /(\w+)\s(\w+)\s(\w+)/; const result = str.match(regex); console.log(result); // ["foo bar baz", "foo", "bar", "baz", ...] console.log(result[1]); // "foo" console.log(result[2]); // "bar" console.log(result[3]); // "baz"
(?:) (非捕获分组)
如果你只需要分组的功能,而不需要捕获匹配到的内容,可以使用非捕获分组 (?:pattern)。这可以提高正则表达式的性能,并避免在 match() 结果中出现不必要的捕获组。
示例:
javascript
const str = "foo bar baz";
const regex = /(?:\w+)\s(\w+)\s(\w+)/;
const result = str.match(regex);
console.log(result); // ["foo bar baz", "bar", "baz", ...]
console.log(result[1]); // "bar" (第一个捕获组现在是 "bar")
反向引用 (\1, \2等)
反向引用允许你在正则表达式的后面部分引用前面捕获组匹配到的内容。\1 引用第一个捕获组,\2 引用第二个捕获组,以此类推。
示例:
javascript
// 匹配重复的单词,例如 "hello hello"
const regex = /(\w+)\s\1/;
console.log(regex.test("hello hello")); // true
console.log(regex.test("hello world")); // false
// 匹配 HTML 标签对,例如 <b>...</b>
const htmlRegex = /<(\w+)>.*<\/\1>/;
console.log(htmlRegex.test("<b>bold</b>")); // true
console.log(htmlRegex.test("<i>italic</i>")); // true
console.log(htmlRegex.test("<b>bold</i>")); // false
选择与或
| 符号在正则表达式中表示"或"的关系,它允许你在多个模式中选择一个进行匹配。
示例:
javascript
// 匹配 "cat" 或 "dog"
const regex = /cat|dog/;
console.log(regex.test("I have a cat.")); // true
console.log(regex.test("I have a dog.")); // true
console.log(regex.test("I have a bird.")); // false
// 结合分组使用,匹配 "gray" 或 "grey"
const colorRegex = /gr(a|e)y/;
console.log(colorRegex.test("gray")); // true
console.log(colorRegex.test("grey")); // true
注意事项:
|的优先级较低,如果需要对多个字符进行选择,通常需要结合()分组使用,以明确匹配范围。 例如,/red|blue apple/会匹配 "red" 或者 "blue apple",而不是 "red apple" 或 "blue apple"。要达到后者效果,应使用/(red|blue) apple/。
零宽断言 (面试高频考点)
零宽断言(Lookahead and Lookbehind Assertions)是正则表达式中非常高级且强大的特性,它们用于在匹配某个模式的同时,不消耗字符串中的字符,只判断某个位置的前面或后面是否满足特定的条件。这使得我们可以在不实际捕获这些条件字符的情况下进行匹配。零宽断言是前端面试中经常考察的难点和亮点。
零宽断言分为四种:
(?=pattern) (正向肯定预查 - Positive Lookahead)
匹配后面紧跟着 pattern 的位置。它会查找 pattern,但不会将 pattern 包含在最终的匹配结果中。
示例:
-
匹配后面跟着数字的字母:
javascriptconst str = "abc123def456"; const regex = /[a-zA-Z](?=\d)/g; // 匹配后面是数字的字母 console.log(str.match(regex)); // ["c", "f"] (匹配 'c' 因为后面是 '1',匹配 'f' 因为后面是 '4') -
匹配货币金额,但只匹配数字部分:
javascriptconst prices = "Price: $100, Cost: $50"; const regex = /\$(?=\d+)/g; // 匹配后面是数字的 '$' 符号 console.log(prices.match(regex)); // ["$", "$"] // 如果想匹配数字本身,可以这样: const numRegex = /\d+(?=\b)/g; // 匹配后面是单词边界的数字 console.log(prices.match(numRegex)); // ["100", "50"]
(?!pattern) (正向否定预查 - Negative Lookahead)
匹配后面不跟着 pattern 的位置。它会查找 pattern,但只有当 pattern 不存在时才匹配。
示例:
-
匹配不是以
.html结尾的字符串:javascriptconst files = "index.html, app.js, style.css, about.html"; const regex = /\b\w+\.(?!html)\w+\b/g; // 匹配不是以 .html 结尾的文件名 console.log(files.match(regex)); // ["app.js", "style.css"] -
匹配不包含特定子字符串的行(通常结合
^和$使用):javascriptconst text = "line one\nline two\nline three"; const regex = /^(?!.*two).*$/gm; // 匹配不包含 "two" 的行 console.log(text.match(regex)); // ["line one", "line three"]
(?<=pattern) (反向肯定预查 - Positive Lookbehind)
匹配前面紧跟着 pattern 的位置。它会查找 pattern,但不会将 pattern 包含在最终的匹配结果中。
注意: JavaScript 的正则表达式在 ES2018 之前不支持反向预查。现在主流浏览器和 Node.js 都已支持。
示例:
-
匹配前面是
$符号的数字:javascriptconst prices = "Price: $100, Cost: $50"; const regex = /(?<=\$)\d+/g; // 匹配前面是 '$' 的数字 console.log(prices.match(regex)); // ["100", "50"]
(?<!pattern) (反向否定预查 - Negative Lookbehind)
匹配前面不跟着 pattern 的位置。它会查找 pattern,但只有当 pattern 不存在时才匹配。
示例:
-
匹配前面不是
http://或https://的www:javascriptconst urls = "www.example.com, http://www.google.com, ftp://www.baidu.com"; const regex = /(?<!https?:\/\/)(www\.\w+\.\w+)/g; // 匹配前面不是 http:// 或 https:// 的域名 console.log(urls.match(regex)); // ["www.example.com", "www.baidu.com"]
零宽断言总结:
| 类型 | 语法 | 含义 | 示例 |
|---|---|---|---|
| 正向肯定预查 | (?=pattern) |
匹配后面是 pattern 的位置 |
\w+(?=\d) 匹配后面是数字的单词 |
| 正向否定预查 | (?!pattern) |
匹配后面不是 pattern 的位置 |
\w+(?!\d) 匹配后面不是数字的单词 |
| 反向肯定预查 | (?<=pattern) |
匹配前面是 pattern 的位置 |
(?<=\$)\d+ 匹配前面是 $ 的数字 |
| 反向否定预查 | (?<!pattern) |
匹配前面不是 pattern 的位置 |
(?<!http:\/\/)\w+ 匹配前面不是 http:// 的单词 |
标志 (Flags)
标志(Flags)是正则表达式的修饰符,它们改变了正则表达式的匹配行为。在JavaScript中,标志通常放在正则表达式字面量的斜杠 / 之后,或者作为 RegExp 构造函数的第二个参数。
g (全局匹配 - Global)
g 标志表示全局匹配。如果设置了 g 标志,正则表达式会查找所有匹配项,而不是在找到第一个匹配项后停止。当与 String.prototype.match() 或 RegExp.prototype.exec() 结合使用时,它的行为尤为重要。
示例:
javascript
const str = "apple banana apple orange";
// 没有 g 标志,只匹配第一个 "apple"
const regex1 = /apple/;
console.log(str.match(regex1)); // ["apple", index: 0, input: "apple banana apple orange", groups: undefined]
// 有 g 标志,匹配所有 "apple"
const regex2 = /apple/g;
console.log(str.match(regex2)); // ["apple", "apple"]
3.4.2 i (忽略大小写 - Ignore Case)
i 标志表示忽略大小写。在进行匹配时,不区分字母的大小写。
示例:
javascript
const str = "Hello World";
// 没有 i 标志,不匹配
const regex1 = /hello/;
console.log(regex1.test(str)); // false
// 有 i 标志,匹配
const regex2 = /hello/i;
console.log(regex2.test(str)); // true
m (多行匹配 - Multiline)
m 标志表示多行匹配。当设置了 m 标志时,^ 和 $ 不仅匹配整个字符串的开头和结尾,还会匹配每一行的开头和结尾(即 \n 或 \r 之后/之前的位置)。
示例:
javascript
const str = "Line 1\nLine 2\nLine 3";
// 没有 m 标志,^ 只匹配字符串开头
const regex1 = /^Line/;
console.log(str.match(regex1)); // ["Line", index: 0, input: "Line 1\nLine 2\nLine 3", groups: undefined]
// 有 m 标志,^ 匹配每行开头
const regex2 = /^Line/gm;
console.log(str.match(regex2)); // ["Line", "Line", "Line"]
// 没有 m 标志,$ 只匹配字符串结尾
const regex3 = /3$/;
console.log(regex3.test(str)); // true
// 有 m 标志,$ 匹配每行结尾
const regex4 = /\d$/gm;
console.log(str.match(regex4)); // ["1", "2", "3"]
u (Unicode支持 - Unicode)
u 标志表示启用完整的 Unicode 支持。这对于处理包含 Unicode 字符(特别是那些超出基本多语言平面 BMP 的字符,如表情符号)的字符串非常重要,它会正确解释这些字符。
示例:
javascript
const str = "\u{1F60A}"; // 微笑表情符号
// 没有 u 标志,\u{1F60A} 被视为两个独立的 UTF-16 代理对
const regex1 = /./;
console.log(regex1.test(str)); // true
console.log(str.match(regex1).length); // 1 (只匹配第一个代理对)
// 有 u 标志,\u{1F60A} 被视为一个完整的 Unicode 字符
const regex2 = /./u;
console.log(regex2.test(str)); // true
console.log(str.match(regex2).length); // 1 (正确匹配整个表情符号)
s (dotAll模式 - DotAll)
s 标志表示 dotAll 模式。当设置了 s 标志时,. (点号) 元字符将匹配包括换行符在内的任何单个字符。在没有 s 标志的情况下,. 不匹配换行符。
示例:
javascript
const str = "foo\nbar";
// 没有 s 标志,. 不匹配换行符
const regex1 = /foo.bar/;
console.log(regex1.test(str)); // false
// 有 s 标志,. 匹配换行符
const regex2 = /foo.bar/s;
console.log(regex2.test(str)); // true
JavaScript中正则表达式的应用
在JavaScript中,正则表达式与 String 对象和 RegExp 对象的方法紧密结合,提供了强大的文本处理能力。
String对象的方法
String 对象提供了几个方法,可以直接使用正则表达式作为参数进行操作。
match()
match() 方法检索字符串,以查找一个或多个与正则表达式匹配的项。它的返回值取决于正则表达式是否带有 g (全局) 标志。
-
无
g标志: 返回一个数组,包含第一个匹配项的完整匹配字符串,以及所有捕获组的匹配结果。数组还包含index(匹配项的起始索引)、input(原始字符串) 和groups(命名捕获组)。如果没有找到匹配项,则返回null。javascriptconst str = "Hello World!"; const regex = /o(rld)/; const result = str.match(regex); console.log(result); /* [ "orld", "rld", index: 7, input: "Hello World!", groups: undefined ] */ console.log(result[0]); // "orld" (完整匹配) console.log(result[1]); // "rld" (第一个捕获组) -
有
g标志: 返回一个包含所有完整匹配项的数组。不包含捕获组信息、index、input等额外属性。如果没有找到匹配项,则返回null。javascriptconst str = "apple banana apple orange"; const regex = /apple/g; const result = str.match(regex); console.log(result); // ["apple", "apple"]
search()
search() 方法用于检索与正则表达式相匹配的子字符串。如果找到匹配项,则返回第一个匹配项的起始索引;否则,返回 -1。
注意: search() 方法会忽略正则表达式的 g 标志。
javascript
const str = "Hello World!";
console.log(str.search(/World/)); // 6
console.log(str.search(/JavaScript/)); // -1
replace()
replace() 方法用于在字符串中用一些字符替换另一些字符,或者替换一个与正则表达式匹配的子字符串。它返回一个新字符串,原始字符串不会被修改。
-
替换第一个匹配项: 如果正则表达式没有
g标志,则只替换第一个匹配项。javascriptconst str = "apple banana apple"; const newStr = str.replace(/apple/, "orange"); console.log(newStr); // "orange banana apple" -
替换所有匹配项: 如果正则表达式有
g标志,则替换所有匹配项。javascriptconst str = "apple banana apple"; const newStr = str.replace(/apple/g, "orange"); console.log(newStr); // "orange banana orange" -
使用替换函数:
replace()方法的第二个参数也可以是一个函数。这个函数会在每次匹配发生时被调用,其返回值将作为替换字符串。这在需要根据匹配内容动态生成替换内容时非常有用。替换函数的参数:
match: 匹配到的完整字符串。p1, p2, ...: 捕获组的匹配结果(如果有)。offset: 匹配项在原始字符串中的起始索引。string: 原始字符串。
javascriptconst str = "foo bar baz"; const newStr = str.replace(/(\w+)/g, (match, p1, offset, string) => { return p1.toUpperCase(); }); console.log(newStr); // "FOO BAR BAZ" // 交换名字的顺序 const name = "John Doe"; const formattedName = name.replace(/(\w+)\s(\w+)/, '$2, $1'); console.log(formattedName); // "Doe, John"
split()
split() 方法使用正则表达式或一个固定字符串作为分隔符,将一个字符串分割成一个字符串数组。
javascript
const str = "one,two;three four";
// 使用逗号或分号作为分隔符
const arr1 = str.split(/[,;]/);
console.log(arr1); // ["one", "two", "three four"]
// 使用空格作为分隔符
const arr2 = str.split(/\s/);
console.log(arr2); // ["one,two;three", "four"]
// 结合捕获组,捕获组的内容会包含在结果数组中
const strWithDelimiters = "apple-banana_orange";
const arr3 = strWithDelimiters.split(/(-|_)/);
console.log(arr3); // ["apple", "-", "banana", "_", "orange"]
RegExp对象的方法
RegExp 对象本身也提供了两个核心方法,用于执行正则表达式匹配操作。
test()
test() 方法用于检测一个字符串是否匹配某个正则表达式。如果匹配成功,返回 true;否则,返回 false。这是最简单的匹配方法,常用于验证输入。
javascript
const regex = /hello/;
console.log(regex.test("hello world")); // true
console.log(regex.test("hi there")); // false
// 验证邮箱格式
const emailRegex = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/;
console.log(emailRegex.test("test@example.com")); // true
console.log(emailRegex.test("invalid-email")); // false
注意: 当正则表达式带有 g 标志时,test() 方法会改变 RegExp 对象的 lastIndex 属性。这意味着连续调用 test() 可能会从上一次匹配结束的位置开始搜索,这在循环中进行多次匹配时需要特别注意。
javascript
const regex = /a/g;
console.log(regex.test("banana")); // true (lastIndex is now 1)
console.log(regex.test("banana")); // true (lastIndex is now 3)
console.log(regex.test("banana")); // true (lastIndex is now 5)
console.log(regex.test("banana")); // false (lastIndex is now 0, reset)
exec()
exec() 方法用于在字符串中执行一个搜索匹配。如果找到匹配项,则返回一个数组,其结构与 String.prototype.match() 在没有 g 标志时的返回值类似(包含完整匹配、捕获组、index、input 和 groups)。如果没有找到匹配项,则返回 null。
exec() 方法的独特之处在于,当正则表达式带有 g 标志时,它会记住上一次匹配的结束位置(通过 lastIndex 属性),并在下一次调用时从该位置继续搜索。这使得 exec() 成为在循环中迭代获取所有匹配项的理想选择。
javascript
const str = "apple banana apple orange";
const regex = /apple/g;
let result;
while ((result = regex.exec(str)) !== null) {
console.log(`Found "${result[0]}" at index ${result.index}. Next search starts at ${regex.lastIndex}`);
}
/*
输出:
Found "apple" at index 0. Next search starts at 5
Found "apple" at index 13. Next search starts at 18
*/
match() vs exec() (带 g 标志):
String.prototype.match()(带g标志):直接返回一个包含所有完整匹配项的数组,不包含捕获组信息和index等。RegExp.prototype.exec()(带g标志):每次调用返回一个匹配项的详细信息(包括捕获组、index等),并且可以通过循环迭代获取所有匹配项。
前端面试高频考点与实战
正则表达式在前端开发中有着广泛的应用,同时也是面试中考察开发者实际解决问题能力的重要环节。以下是一些常见的前端面试考点和实战案例。
邮箱验证
邮箱验证是前端表单验证中最常见的场景之一。一个合格的邮箱正则表达式需要考虑到各种合法邮箱格式的复杂性。
正则表达式:
regex
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
解析:
^: 匹配字符串的开始。[a-zA-Z0-9._%+-]+: 匹配用户名部分。允许字母、数字、点、下划线、百分号、加号、减号,并且至少出现一次。@: 匹配@符号。[a-zA-Z0-9.-]+: 匹配域名部分。允许字母、数字、点、减号,并且至少出现一次。\.: 转义点号,匹配域名和顶级域名之间的点。[a-zA-Z]{2,}: 匹配顶级域名(如 com, org, cn 等),至少两个字母。$: 匹配字符串的结束。
示例:
javascript
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
console.log(emailRegex.test("test@example.com")); // true
console.log(emailRegex.test("john.doe@sub.domain.co.uk")); // true
console.log(emailRegex.test("invalid-email")); // false
console.log(emailRegex.test("test@.com")); // false
手机号验证
手机号验证通常需要根据不同国家或地区的规则来制定。这里以中国大陆的手机号为例(11位数字,以1开头,第二位通常是3-9)。
正则表达式:
regex
/^1[3-9]\d{9}$/
解析:
^: 匹配字符串的开始。1: 匹配数字1。[3-9]: 匹配第二位数字,范围是3到9。\d{9}: 匹配后面九位数字。$: 匹配字符串的结束。
示例:
javascript
const phoneRegex = /^1[3-9]\d{9}$/;
console.log(phoneRegex.test("13812345678")); // true
console.log(phoneRegex.test("19987654321")); // true
console.log(phoneRegex.test("12345678901")); // false (第二位不是3-9)
console.log(phoneRegex.test("1381234567")); // false (不足11位)
URL解析
URL解析是一个相对复杂的任务,通常需要提取协议、域名、端口、路径、查询参数和哈希等部分。
一个简化的URL解析正则表达式:
regex
/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?(\?[^#]*)?(#.*)?$/
解析(部分):
^(https?:\/\/)?: 匹配可选的http://或https://协议。([\da-z\.-]+)\.([a-z\.]{2,6}): 匹配域名和顶级域名。([\/\w \.-]*)*\/?: 匹配可选的路径。(\?[^#]*)?: 匹配可选的查询参数。(#.*)?: 匹配可选的哈希。
示例:
javascript
const urlRegex = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?(\?[^#]*)?(#.*)?$/;
console.log(urlRegex.test("http://www.example.com")); // true
console.log(urlRegex.test("https://example.com/path/to/page?id=123#section")); // true
console.log(urlRegex.test("www.example.com")); // true
console.log(urlRegex.test("invalid-url")); // false
密码强度校验
密码强度校验通常要求密码包含大小写字母、数字、特殊字符,并有最小长度限制。
正则表达式(至少8位,包含大小写字母、数字和特殊字符):
regex
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,}$/
解析:
^: 匹配字符串的开始。(?=.*[a-z]): 正向肯定预查,确保字符串中至少包含一个小写字母。(?=.*[A-Z]): 正向肯定预查,确保字符串中至少包含一个大写字母。(?=.*\d): 正向肯定预查,确保字符串中至少包含一个数字。(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]): 正向肯定预查,确保字符串中至少包含一个特殊字符。.{8,}: 匹配任意字符至少8次。$: 匹配字符串的结束。
示例:
javascript
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,}$/;
console.log(passwordRegex.test("Password123!")); // true
console.log(passwordRegex.test("password123")); // false (缺少大写字母和特殊字符)
console.log(passwordRegex.test("Pass1!")); // false (长度不足8位)
敏感词过滤
敏感词过滤通常需要匹配一个或多个敏感词,并将其替换为星号或其他占位符。
正则表达式:
javascript
const sensitiveWords = ["敏感词A", "敏感词B", "违禁词C"];
const sensitiveRegex = new RegExp(sensitiveWords.join("|"), "g");
const text = "这是一段包含敏感词A和违禁词C的文本。";
const filteredText = text.replace(sensitiveRegex, "**");
console.log(filteredText); // "这是一段包含**和**的文本。"
解析:
- 通过
sensitiveWords.join("|")将敏感词数组转换为敏感词A|敏感词B|违禁词C这样的模式,利用|进行选择匹配。 g标志确保所有匹配到的敏感词都被替换。
模板字符串解析
在前端开发中,我们经常需要解析模板字符串,例如 Hello, {{name}}!,将 {{name}} 替换为实际的值。
正则表达式:
regex
/\{\{(\w+)\}\} /g
示例:
javascript
const template = "Hello, {{name}}! Your age is {{age}}.";
const data = { name: "Alice", age: 30 };
const parsedTemplate = template.replace(/\{\{(\w+)\}\} /g, (match, key) => {
return data[key];
});
console.log(parsedTemplate); // "Hello, Alice! Your age is 30."
解析:
\{\{: 匹配字面量{{。(\w+): 捕获组,匹配一个或多个单词字符(即变量名)。\}\}: 匹配字面量}}。g: 全局匹配,确保所有{{...}}都被替换。- 替换函数中,
key参数就是捕获组(\w+)匹配到的内容,即变量名。
驼峰命名与烤串命名转换
在JavaScript中,经常需要在驼峰命名(camelCase)和烤串命名(kebab-case)之间进行转换。
驼峰命名转烤串命名
正则表达式:
regex
/[A-Z]/g
示例:
javascript
function camelToKebab(name) {
return name.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
}
console.log(camelToKebab("userName")); // "user-name"
console.log(camelToKebab("firstName")); // "first-name"
console.log(camelToKebab("backgroundColor")); // "background-color"
解析:
- 匹配所有大写字母。
- 替换函数将匹配到的大写字母转换为小写,并在前面加上
-。
烤串命名转驼峰命名
正则表达式:
regex
/-(.)/g
示例:
javascript
function kebabToCamel(name) {
return name.replace(/-(.)/g, (match, p1) => p1.toUpperCase());
}
console.log(kebabToCamel("user-name")); // "userName"
console.log(kebabToCamel("first-name")); // "firstName"
console.log(kebabToCamel("background-color")); // "backgroundColor"
解析:
- 匹配
-后面跟着的任意一个字符,并将该字符捕获到第一个捕获组p1。 - 替换函数将捕获到的字符
p1转换为大写。
性能优化与注意事项
正则表达式虽然强大,但如果使用不当,也可能导致性能问题,甚至引发"灾难性回溯"(Catastrophic Backtracking)。了解如何优化正则表达式的性能至关重要。
避免过度回溯
当正则表达式中包含嵌套的量词,并且这些量词可以匹配空字符串时,就可能导致过度回溯。例如,/(a+)+b/ 匹配 aaaaab 这样的字符串时,引擎会尝试多种匹配路径,导致性能急剧下降。
示例:
regex
/(a+)+b/ // 避免这种模式
优化建议:
- 使用非贪婪模式: 在某些情况下,使用非贪婪量词
*?,+?,??可以减少回溯。 - 避免嵌套量词: 尽量避免
(X+)+或(X*)*这种形式的嵌套量词。 - 使用原子组(Atomic Grouping): 某些正则表达式引擎支持原子组
(?>pattern),它会阻止回溯。但 JavaScript 不直接支持原子组,可以通过其他方式模拟或重写。 - 具体化匹配: 尽可能使用更具体的字符类或字符,而不是宽泛的
.。
优化正则表达式的写法
- 使用字符类而不是
|: 当匹配单个字符时,[abc]比a|b|c更高效。 - 将常用模式放在前面: 在使用
|进行选择时,将最常匹配的模式放在前面,可以更快地找到匹配项。 - 减少不必要的捕获组: 如果不需要捕获某个分组的内容,使用非捕获分组
(?:...)可以提高性能,因为它不需要存储匹配结果。 - 避免不必要的括号: 只有在需要分组或捕获时才使用括号。
- 锚点使用: 尽可能使用
^和$来锚定匹配的开始和结束,这可以大大减少不必要的搜索范围。
适当使用非捕获分组
如前所述,非捕获分组 (?:...) 可以在只需要分组功能而不需要捕获内容时使用。这不仅可以避免 match() 结果中出现不必要的捕获组,还能在一定程度上提升性能,因为引擎不需要为这些分组分配存储空间。
示例:
javascript
// 捕获分组,会增加 match 结果的长度
const regex1 = /(foo)(bar)/;
console.log("foobar".match(regex1)); // ["foobar", "foo", "bar", ...]
// 非捕获分组,只关注整体匹配
const regex2 = /(?:foo)(?:bar)/;
console.log("foobar".match(regex2)); // ["foobar", ...]
总结
学习正则表达式并非一蹴而就,它需要大量的练习和实践。建议你在理解基本概念的基础上,多动手编写和调试正则表达式,尝试解决各种实际问题。同时,也可以利用在线正则表达式测试工具(如 Regex101、RegExr)来辅助学习和调试。
希望本文能成为你学习正则表达式的"宝典",助你在前端开发的道路上更进一步!