MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的正则表达式:深入理解和高效使用

2021-09-013.7k 阅读

一、正则表达式基础概念

在JavaScript中,正则表达式是用于匹配和处理文本的强大工具。正则表达式使用一种专门的语法来描述模式,这种模式可以用来检查一个字符串是否包含特定的字符组合,或者从字符串中提取符合条件的子字符串。

正则表达式由两种基本字符类型组成:原义(正常)字符和元字符。原义字符就是其字面意义的字符,例如字母、数字和标点符号。而元字符则具有特殊的含义,用于定义匹配模式的规则。

1.1 元字符

常见的元字符有很多,下面介绍一些最常用的:

  • 点号(.):匹配除换行符之外的任何单个字符。例如,正则表达式a.c可以匹配abca1ca!c等,但不能匹配a\nc(其中\n表示换行符)。
const str1 = "abc";
const str2 = "a1c";
const str3 = "a\nc";
const regex = /a.c/;
console.log(regex.test(str1)); // true
console.log(regex.test(str2)); // true
console.log(regex.test(str3)); // false
  • 星号(*):匹配前面的字符零次或多次。例如,a*可以匹配空字符串,也可以匹配aaaaaa等。
const str4 = "";
const str5 = "a";
const str6 = "aaa";
const regex2 = /a*/;
console.log(regex2.test(str4)); // true
console.log(regex2.test(str5)); // true
console.log(regex2.test(str6)); // true
  • 加号(+):匹配前面的字符一次或多次。与*不同,+要求至少匹配一次。例如,a+可以匹配aaaaaa等,但不能匹配空字符串。
const str7 = "";
const str8 = "a";
const str9 = "aaa";
const regex3 = /a+/;
console.log(regex3.test(str7)); // false
console.log(regex3.test(str8)); // true
console.log(regex3.test(str9)); // true
  • 问号(?):匹配前面的字符零次或一次。例如,a?可以匹配空字符串或a
const str10 = "";
const str11 = "a";
const regex4 = /a?/;
console.log(regex4.test(str10)); // true
console.log(regex4.test(str11)); // true
  • 方括号([]):用于定义字符类。在方括号内列出的字符,匹配其中任意一个字符。例如,[abc]可以匹配abc。也可以使用连字符(-)表示字符范围,如[a - z]匹配任意小写字母。
const str12 = "a";
const str13 = "b";
const str14 = "d";
const regex5 = /[abc]/;
console.log(regex5.test(str12)); // true
console.log(regex5.test(str13)); // true
console.log(regex5.test(str14)); // false
  • 脱字符(^):在方括号内使用时,表示取反。例如,[^abc]匹配除了abc之外的任何字符。在正则表达式的开头使用时,表示匹配字符串的开始位置。
const str15 = "d";
const str16 = "a";
const regex6 = /[^abc]/;
console.log(regex6.test(str15)); // true
console.log(regex6.test(str16)); // false
  • 美元符号($):匹配字符串的结束位置。例如,abc$匹配以abc结尾的字符串。
const str17 = "123abc";
const str18 = "abc123";
const regex7 = /abc$/;
console.log(regex7.test(str17)); // true
console.log(regex7.test(str18)); // false
  • 竖线(|):表示或的关系。例如,a|b可以匹配ab
const str19 = "a";
const str20 = "c";
const regex8 = /a|b/;
console.log(regex8.test(str19)); // true
console.log(regex8.test(str20)); // false
  • 圆括号(()):用于分组和捕获。分组是将多个字符视为一个整体,例如,(ab)+可以匹配abababababab等。捕获是指在匹配时可以记住括号内的内容,以便后续使用。
const str21 = "abab";
const regex9 = /(ab)+/;
console.log(regex9.test(str21)); // true

1.2 字符转义

当需要匹配元字符本身时,需要对其进行转义。在正则表达式中,使用反斜杠(\)来转义元字符。例如,要匹配点号(.),正则表达式应该写成\.;要匹配星号(*),应该写成\*

const str22 = ".";
const regex10 = /\./;
console.log(regex10.test(str22)); // true

二、创建正则表达式对象

在JavaScript中有两种方式创建正则表达式对象:字面量形式和构造函数形式。

2.1 字面量形式

使用一对斜杠(/)来定义正则表达式。例如,/abc/表示匹配字符串abc的正则表达式。

const regex11 = /abc/;
const str23 = "abcdef";
console.log(regex11.test(str23)); // true

2.2 构造函数形式

使用RegExp构造函数来创建正则表达式对象。构造函数接受一个字符串参数,该字符串表示正则表达式的模式。例如,new RegExp("abc")/abc/是等效的。

const regex12 = new RegExp("abc");
const str24 = "abc123";
console.log(regex12.test(str24)); // true

当使用构造函数形式时,如果正则表达式中包含特殊字符,需要进行双重转义。例如,要匹配点号(.),需要写成new RegExp("\\."),因为在字符串中反斜杠本身也需要转义。

const regex13 = new RegExp("\\.");
const str25 = ".";
console.log(regex13.test(str25)); // true

三、正则表达式的修饰符

JavaScript正则表达式支持几种修饰符,这些修饰符可以改变正则表达式的匹配行为。

3.1 g修饰符(全局匹配)

g修饰符表示全局匹配,即匹配字符串中所有符合模式的子字符串,而不仅仅是第一个。

const str26 = "abab";
const regex14 = /ab/g;
const matches = str26.match(regex14);
console.log(matches); // ['ab', 'ab']

3.2 i修饰符(忽略大小写)

i修饰符使正则表达式在匹配时忽略大小写。例如,/a/i可以匹配aA

const str27 = "Apple";
const regex15 = /a/i;
console.log(regex15.test(str27)); // true

3.3 m修饰符(多行匹配)

m修饰符用于多行匹配。在默认情况下,^$分别匹配字符串的开始和结束位置。使用m修饰符后,^还会匹配每行的开始位置,$还会匹配每行的结束位置。

const str28 = "line1\nline2";
const regex16 = /^line/m;
const matches2 = str28.match(regex16);
console.log(matches2); // ['line', index: 0, input: 'line1\nline2', groups: undefined]

3.4 s修饰符(dotAll模式)

s修饰符使得点号(.)可以匹配包括换行符在内的任意字符。在没有s修饰符时,点号不匹配换行符。

const str29 = "a\nb";
const regex17 = /a.b/s;
console.log(regex17.test(str29)); // true

3.5 u修饰符(Unicode模式)

u修饰符用于处理Unicode字符。在JavaScript中,有些Unicode字符需要用4个十六进制数字表示(例如,\u{1F600}表示笑脸表情)。使用u修饰符可以正确处理这些字符。

const str30 = "😀";
const regex18 = /^\u{1F600}$/u;
console.log(regex18.test(str30)); // true

3.6 y修饰符(粘连匹配)

y修饰符表示粘连匹配,它要求匹配从lastIndex属性指定的位置开始,并且如果匹配成功,lastIndex会更新到下一个匹配的开始位置。

const str31 = "aaa";
const regex19 = /a/y;
regex19.lastIndex = 1;
console.log(regex19.test(str31)); // false
regex19.lastIndex = 0;
console.log(regex19.test(str31)); // true

四、正则表达式的方法

JavaScript为正则表达式对象和字符串对象都提供了一些方法来进行匹配和处理操作。

4.1 正则表达式对象的方法

  • test():用于测试一个字符串是否匹配某个正则表达式。如果匹配返回true,否则返回false。前面已经有很多使用test()方法的示例。
  • exec():在字符串中执行查找匹配的操作,并返回一个数组,该数组包含匹配的结果及相关信息。如果没有找到匹配,则返回null
const str32 = "abc123";
const regex20 = /abc/;
const result = regex20.exec(str32);
console.log(result); // ['abc', index: 0, input: 'abc123', groups: undefined]

当正则表达式使用g修饰符时,exec()方法会多次执行查找,每次执行会更新lastIndex属性。

const str33 = "abab";
const regex21 = /ab/g;
let match;
while (match = regex21.exec(str33)) {
    console.log(`匹配到: ${match[0]}, 位置: ${match.index}`);
}
// 输出:
// 匹配到: ab, 位置: 0
// 匹配到: ab, 位置: 2

4.2 字符串对象的方法

  • match():在字符串中查找匹配正则表达式的子字符串,并返回一个包含所有匹配结果的数组。如果没有找到匹配,则返回null
const str34 = "abc def abc";
const regex22 = /abc/g;
const matches3 = str34.match(regex22);
console.log(matches3); // ['abc', 'abc']
  • search():在字符串中查找匹配正则表达式的子字符串,并返回第一个匹配结果的索引。如果没有找到匹配,则返回-1
const str35 = "hello world";
const regex23 = /world/;
const index = str35.search(regex23);
console.log(index); // 6
  • replace():用于在字符串中替换匹配正则表达式的子字符串。它接受两个参数,第一个是正则表达式,第二个是用于替换的字符串或函数。
const str36 = "abc123abc";
const newStr = str36.replace(/abc/g, "xyz");
console.log(newStr); // xyz123xyz

当第二个参数是函数时,函数的参数包含匹配到的子字符串、捕获组(如果有)等信息,函数返回值作为替换字符串。

const str37 = "1 2 3";
const newStr2 = str37.replace(/\d/g, function (match) {
    return parseInt(match) * 2;
});
console.log(newStr2); // 2 4 6
  • split():根据正则表达式将字符串分割成数组。
const str38 = "a,b;c d";
const parts = str38.split(/[,\s;]+/);
console.log(parts); // ['a', 'b', 'c', 'd']

五、深入理解正则表达式的捕获组

捕获组是正则表达式中用圆括号(())括起来的部分。捕获组可以记住匹配的子字符串,以便在后续操作中使用。

5.1 编号捕获组

在正则表达式中,第一个左括号开始的捕获组编号为1,第二个为2,以此类推。在使用exec()match()方法时,返回的数组中除了第一个元素是整个匹配的字符串外,后面的元素依次是各个捕获组匹配的字符串。

const str39 = "John, 25";
const regex24 = /(\w+), (\d+)/;
const result2 = regex24.exec(str39);
console.log(result2[1]); // John
console.log(result2[2]); // 25

5.2 命名捕获组

从ES2018开始,JavaScript支持命名捕获组。命名捕获组使用(?<name>pattern)的语法,其中name是捕获组的名称,pattern是匹配模式。

const str40 = "John, 25";
const regex25 = /(?<name>\w+), (?<age>\d+)/;
const result3 = regex25.exec(str40);
console.log(result3.groups.name); // John
console.log(result3.groups.age); // 25

5.3 反向引用

反向引用是指在正则表达式中引用之前捕获组匹配的内容。在编号捕获组中,使用\nn是捕获组的编号)来进行反向引用。例如,(\w+)\1可以匹配两个连续相同的单词。

const str41 = "hello hello";
const str42 = "hello world";
const regex26 = /(\w+)\1/;
console.log(regex26.test(str41)); // true
console.log(regex26.test(str42)); // false

对于命名捕获组,使用\k<name>进行反向引用。

const str43 = "abc abc";
const regex27 = /(?<word>\w+)\k<word>/;
console.log(regex27.test(str43)); // true

六、正则表达式的边界匹配

边界匹配用于指定匹配应该发生在字符串的特定位置,除了前面提到的^$之外,还有一些其他的边界匹配元字符。

6.1 \b(单词边界)

\b匹配单词边界,即单词和非单词字符之间的位置,或者字符串的开始/结束位置且该位置紧接着/紧跟一个单词字符。例如,\bcat\b可以匹配"the cat"中的cat,但不匹配"category"中的cat

const str44 = "the cat is cute";
const str45 = "category";
const regex28 = /\bcat\b/;
console.log(regex28.test(str44)); // true
console.log(regex28.test(str45)); // false

6.2 \B(非单词边界)

\B\b相反,匹配非单词边界,即两个单词字符之间或两个非单词字符之间的位置。例如,\Bcat\B可以匹配"category"中的cat,但不匹配"the cat"中的cat

const str46 = "the cat is cute";
const str47 = "category";
const regex29 = /\Bcat\B/;
console.log(regex29.test(str46)); // false
console.log(regex29.test(str47)); // true

6.3 \A(字符串开始)

\A匹配字符串的开始位置,与^类似,但^在多行模式下会匹配每行的开始,而\A始终只匹配整个字符串的开始。

const str48 = "line1\nline2";
const regex30 = /\Aline/;
const matches4 = str48.match(regex30);
console.log(matches4); // ['line', index: 0, input: 'line1\nline2', groups: undefined]

6.4 \Z(字符串结束)

\Z匹配字符串的结束位置,与$类似,但$在多行模式下会匹配每行的结束,而\Z始终只匹配整个字符串的结束。如果字符串以换行符结尾,\Z会匹配换行符之前的位置。

const str49 = "line1\n";
const regex31 = /line1\Z/;
console.log(regex31.test(str49)); // true

七、正则表达式的性能优化

在使用正则表达式时,性能是一个需要考虑的重要因素。以下是一些优化正则表达式性能的方法:

7.1 简化正则表达式

尽量简化正则表达式的模式,避免不必要的复杂结构。例如,如果只需要匹配数字,可以直接使用\d,而不是使用[0 - 9],虽然两者功能相同,但\d在解析和匹配时可能更高效。

// 推荐
const regex32 = /\d+/;
// 不推荐
const regex33 = /[0 - 9]+/;

7.2 减少回溯

回溯是正则表达式匹配过程中的一种机制,当匹配失败时,正则表达式引擎会尝试回溯到之前的状态并重新尝试其他可能的匹配路径。过多的回溯会导致性能下降。可以通过合理使用量词(如*+?)和分组来减少回溯。例如,使用非贪婪量词(*?+?)可以让量词尽可能少地匹配字符,从而减少回溯的可能性。

const str50 = "<div>content1</div><div>content2</div>";
// 贪婪匹配,可能会产生较多回溯
const regex34 = /<div>.*<\/div>/;
// 非贪婪匹配,减少回溯
const regex35 = /<div>.*?<\/div>/;

7.3 预编译正则表达式

如果需要多次使用同一个正则表达式,建议使用字面量形式或提前使用RegExp构造函数进行预编译,而不是每次都动态创建正则表达式对象。

// 预编译
const regex36 = /abc/g;
function matchMany(str) {
    return str.match(regex36);
}
// 不推荐每次动态创建
function matchManyBad(str) {
    return str.match(new RegExp("abc", "g"));
}

7.4 避免不必要的捕获组

捕获组会增加正则表达式的复杂度和内存开销。如果不需要使用捕获组的内容,尽量避免使用。例如,如果只是想匹配某个模式而不关心具体的子字符串,可以使用非捕获组(?:pattern)

// 非捕获组
const regex37 = /(?:abc)+/;
// 捕获组,不必要时应避免
const regex38 = /(abc)+/;

八、正则表达式在实际场景中的应用

正则表达式在JavaScript开发中有广泛的应用场景,下面介绍一些常见的应用。

8.1 表单验证

在Web开发中,经常需要对用户输入的表单数据进行验证。例如,验证邮箱地址、手机号码、密码强度等。

// 邮箱验证
function validateEmail(email) {
    const regex = /^[a-zA-Z0 - 9_.+-]+@[a-zA-Z0 - 9 -]+\.[a-zA-Z0 - 9-.]+$/;
    return regex.test(email);
}
console.log(validateEmail("test@example.com")); // true
console.log(validateEmail("test.example.com")); // false
// 手机号码验证(简单示例,实际可能需要更复杂规则)
function validatePhone(phone) {
    const regex = /^1[3 - 9]\d{9}$/;
    return regex.test(phone);
}
console.log(validatePhone("13800138000")); // true
console.log(validatePhone("12345678901")); // false

8.2 文本提取

从一段文本中提取特定格式的信息。例如,从HTML文档中提取所有链接。

const html = '<a href="https://example.com">Example</a><a href="https://google.com">Google</a>';
const regex = /<a href="([^"]+)"/g;
let match;
while (match = regex.exec(html)) {
    console.log(match[1]);
}
// 输出:
// https://example.com
// https://google.com

8.3 数据清洗和格式化

对不规范的数据进行清洗和格式化。例如,将字符串中的所有数字提取出来并转换为数组。

const str51 = "abc123def456";
const numbers = str51.match(/\d+/g).map(Number);
console.log(numbers); // [123, 456]

九、正则表达式的陷阱与注意事项

在使用正则表达式时,有一些常见的陷阱和需要注意的地方。

9.1 特殊字符的转义

前面已经提到,对元字符进行转义时要特别小心。尤其是在使用构造函数形式创建正则表达式时,需要进行双重转义。忘记转义或转义错误可能导致正则表达式无法按预期工作。

// 错误,未转义点号
const regex39 = new RegExp("a.b");
const str52 = "a1b";
console.log(regex39.test(str52)); // true,不符合预期
// 正确
const regex40 = new RegExp("a\\.b");
console.log(regex40.test(str52)); // false

9.2 贪婪与非贪婪模式

量词的贪婪与非贪婪模式可能会导致不同的匹配结果,要根据实际需求选择合适的模式。在处理HTML等标记语言时,贪婪模式可能会匹配过多的内容。

const html2 = "<div>content</div><div>more content</div>";
// 贪婪模式,匹配整个字符串
const regex41 = /<div>.*<\/div>/;
const match1 = html2.match(regex41);
console.log(match1[0]); // <div>content</div><div>more content</div>
// 非贪婪模式,只匹配第一个div块
const regex42 = /<div>.*?<\/div>/;
const match2 = html2.match(regex42);
console.log(match2[0]); // <div>content</div>

9.3 全局匹配与lastIndex

当使用全局匹配(g修饰符)时,lastIndex属性会影响exec()match()方法的行为。每次调用exec()后,lastIndex会更新到下一个匹配的开始位置。如果不小心处理lastIndex,可能会导致匹配结果不符合预期。

const str53 = "abab";
const regex43 = /ab/g;
console.log(regex43.exec(str53)[0]); // ab
console.log(regex43.lastIndex); // 2
console.log(regex43.exec(str53)[0]); // ab
console.log(regex43.lastIndex); // 4

9.4 性能问题

如前面提到的,复杂的正则表达式可能会导致性能问题。在处理大量文本时,要注意优化正则表达式,避免不必要的回溯和复杂结构。

十、正则表达式的高级技巧

除了基本的使用方法外,还有一些高级技巧可以帮助更好地利用正则表达式。

10.1 前瞻和后顾

前瞻和后顾用于在匹配某个模式之前或之后断言另一个模式是否存在,但并不包含断言的内容在匹配结果中。

  • 正前瞻(?=pattern):断言在当前位置之后会出现pattern。例如,a(?=b)可以匹配ab中的a,但不匹配ac中的a
const str54 = "ab";
const regex44 = /a(?=b)/;
console.log(regex44.test(str54)); // true
  • 负前瞻(?!pattern):断言在当前位置之后不会出现pattern。例如,a(?!b)可以匹配ac中的a,但不匹配ab中的a
const str55 = "ac";
const regex45 = /a(?!b)/;
console.log(regex45.test(str55)); // true
  • 正后顾(?<=pattern):断言在当前位置之前出现过pattern。例如,(?<=a)b可以匹配ab中的b,但不匹配cb中的b。注意,JavaScript目前对正后顾的支持有限,只有在某些特定环境下(如Chrome浏览器)才支持。
// 在支持正后顾的环境下
const str56 = "ab";
const regex46 = /(?<=a)b/;
console.log(regex46.test(str56)); // true
  • 负后顾(?<!pattern):断言在当前位置之前没有出现过pattern。同样,JavaScript对负后顾的支持也有限。

10.2 条件判断

在正则表达式中可以使用条件判断来根据前面的捕获组匹配结果决定后续的匹配模式。语法为(?(id/name)yes-pattern|no-pattern),其中id/name是捕获组的编号或名称,yes-pattern是捕获组匹配时使用的模式,no-pattern是捕获组未匹配时使用的模式(|no-pattern部分可选)。

// 简单示例,实际应用可能更复杂
const regex47 = /(\d+)?(?(1)\D+|\d+)/;
console.log(regex47.test("123")); // true
console.log(regex47.test("abc")); // true
console.log(regex47.test("123abc")); // true

10.3 递归匹配

递归匹配可以用于处理具有递归结构的文本,例如XML或JSON。虽然JavaScript正则表达式对递归匹配的支持有限,但通过一些技巧可以实现简单的递归匹配。例如,对于简单的嵌套括号结构:

const str57 = "((abc))";
const regex48 = /\((.*?)\)/;
function recursiveMatch(str) {
    let result = regex48.exec(str);
    while (result) {
        str = str.replace(result[0], recursiveMatch(result[1]));
        result = regex48.exec(str);
    }
    return str;
}
console.log(recursiveMatch(str57)); // abc

通过深入理解和掌握这些正则表达式的知识和技巧,在JavaScript开发中可以更高效地处理文本,实现各种复杂的匹配和处理需求。无论是进行数据验证、文本提取还是其他文本相关的任务,正则表达式都是一个强大而不可或缺的工具。在实际应用中,要根据具体情况灵活运用,并注意性能优化和避免常见的陷阱。