细说JavaScript正则表达式(RegExp)

搜索、匹配和聚合是日常网络活动的重要组成部分,例如,当浏览或搜索某些关键字时,会进行大量搜索。为了使 搜索/匹配 高效和精确,像 VsCode 和 Sublime 这样的流行编辑器都是使用正则表达式来支持搜索和替换。因此,在使用这些编辑器的时候,当按下 CTRL + F 组合键时,就可以搜索和匹配选择的文本。

除了搜索之外,还可以使用正则表达式执行表单输入验证。例如,可以检查用户输入的 手机号码 是否全是数字,或者输入的 密码 是否包含特殊字符等。很多开发人员喜欢使用正则表达式(RegExp),是因为可以提高效率,并且不受编程语言的限制。用 JavaScript 编写的 RegExp 可以很容易地迁移到 golang 或者 Python 中。

昨天在文章《JavaScript 正则表达式的 5 个方法》介绍了JavaScript中正则表达式涉及的5个方法,在本文将解释 JavaScript 中的 RegExp 是什么、它的重要性、特殊字符、如何有效地创建和编写它们、主要用例以及它的不同属性和方法。

什么是正则表达式

正则表达式是一个字符序列,用于在文本匹配/搜索的字符串中匹配字符组合。在 JavaScript中,正则表达式是从字符序列中搜索模式,并且正则表达式也是对象。

RegExp 使字符串的搜索和匹配变得更容易和更快。例如,在搜索引擎、日志、编辑器等中,可以轻松高效地过滤/匹配文本。这就是 RegExp 模式的用武之地,用一系列字符定义搜索模式。

正则表达式的重要性

随着数字化转型的加速,信息已成为越来越多行业的重要组成部分。从这里开始将探讨正则表达式为什么重要以及它们在数据管理中是如何发挥其优势。

字符串的搜索/匹配

大多数使用正则表达式的开发人员都使用它搜索和匹配字符串。RegExp 允许在其他文本池中搜索文本。当使用 RegExp 搜索文本时,如果找到文本,将得到 true 或 false。当试图从一组文本中匹配一个文本时,会得到一个带有预期文本的数组,即与搜索的模式匹配的文本。

表单输入验证

表单输入验证是很多项目都会涉及的需求,对于前端来说更加常见。例如希望用户输入的 手机号码 为数字,并且希望电子邮箱的格式为 @xxx.com。对于此类需求首先想到的就是正则表达式。

来看看下面的 RegExp 示例来验证用户的输入:

const mobile = 13000000000;
const regex = new RegExp("[0-9]");
console.log(regex.test(mobile)); // true

上面的代码将输出 true,因为 mobile 是 0-9 之间的数字组成,当然手机号码还有别的规则,这里就不展开了。

网页抓取

网页抓取涉及从网站中提取数据。使用 RegExp,可以轻松完成此需求。例如,可以通过抓取网页源代码并提取与其模式匹配的数据来提取子字符串,如标题、网址等等。

数据整理

可以对从网页中检索到的数据执行更多操作。例如,可以评估来自网络的数据并将其排列成所需的格式,以便做出正确的决策。使用 RegExp,可以聚合和映射数据以将其用于分析目的。现在有些新冠疫情数据就是通过爬虫进行抓取,然后进行聚合和映射进行分析。

代码生成

使用正则表达是生成代码,可以大大提高编码效率,前提是需要对正则表达式达到精通的水平。

如何创建 RegExp 对象

JavaScript 中的正则表达式是使用 RegExp 对象创建的。因此,正则表达式大多是 JavaScript 对象。上面理解了正则表达式是什么,现在看看如何在 JavaScript 中创建它们。

构造函数

在 JavaScript 中创建正则表达式的另一种方式是使用构造函数,此方法将正则表达式作为函数参数中的字符串。从ECMAScript 6开始,构造函数现在可以接受正则表达式字面量。

建议在创建模式将在运行时发送变化的正则表达式的情况下使用构造函数。例如,在验证用户输入或执行迭代时。使用构造函数创建 JavaScript 正则表达式的语法如下:

const regexp = new RegExp("hello", "g"); // 字符串模式的构造函数
const regexp = new RegExp(/hello/, "g"); // 带正则表达式的构造函数

就像《JavaScript 正则表达式的 5 个方法》中的文字表示法示例一样,将使用 RegExp 构造函数创建区分大小写的搜索:

const strText = "Hello China";
const regex = new RegExp("hello");
console.log(regex.test(strText)); // false

那么常用的标记 g 和 i ,在构造函数的方式下如何定义,如下代码:

  • 标记 g :全局匹配模式
  • 标记 i :不区分大小写模式
const strText = "Hello China";
const regex = new RegExp("hello", "i");
console.log(regex.test(strText)); // true

正则表达式方法

正则表达式有两种主要方法,分别是 exec() 和 test() 。然而,还有用于正则表达式的字符串的其他方法,比如 match()matchAll()replace()replaceAll()search() 和 split() 。从这里开始将探索可用于 JavaScript 正则表达式的不同方法,在文章《JavaScript 正则表达式的 5 个方法》介绍了一些,本文就不重复介绍了。

exec()

此方法执行搜索并返回结果数组或空值,用于对文本字符串中的多个匹配项进行迭代。例如,下面将使用 exec() 方法进行迭代和不进行迭代的实例:

没有迭代

const regex = RegExp("chin*", "g");
const strText = "hello china,i love china";

const result = regex.exec(strText);
console.log(result); // ['chin',index: 6,input: 'hello china,i love china',groups: undefined]

迭代方式

const regex = RegExp("chin*", "g");
const strText = "hello china,i love china";
let arrayResult = [];
while ((arrayResult = regex.exec(strText)) !== null) {
    console.log(`找到 ${arrayResult[0]},下一次搜索开始索引值:${regex.lastIndex}。`);
    // 找到 chin,下一次搜索开始索引值:10。
    // 找到 chin,下一次搜索开始索引值:23。
}

从运行情况可以看出,在没有迭代的情况下,仅获得第一个匹配项的索引。而通过迭代,可以得到所有(多个)匹配的结果。

matchAll()

matchAll() 方法必须与全局标记 g 一起使用,与 match() 方法之间的区别在于能够返回包含所有匹配组和捕获组的迭代器。在 match() 方法中,不返回带有 g 标记的捕获组。如果方法中没有标记 g ,match() 则返回第一个匹配项以及相关的捕获组。

对于 matchAll() 方法,g 标记的使用非常重要,否则,将会出现异常。下面是 match() 方法和 matchAll() 方法使用示例代码:

match() 的实例代码:

const strText = "Hello China,I love China";
const regex = /Ch(i)[a-z]/g;
const found = strText.match(regex);

console.log(found); // [ 'Chin', 'Chin' ]

matchAll() 的实例代码:

const strText = "Hello China,I love China";
const regex = /Ch(i)[a-z]/g;
const found = strText.matchAll(regex);

Array.from(found, (item) => console.log(item));
// ['Chin','i',index: 6,input: 'Hello China,I love China',groups: undefined]
// ['Chin','i',index: 6,input: 'Hello China,I love China',groups: undefined]

从上面的实例代码运行结果看到返回了捕获组 i 。这在 match() 方法的实例代码中没有返回。matchAll() 在语法上和 match() 相似。

split()

使用 split() 方法从可以从字符串中提取子字符串,这个方法的作用是根据提供的模式将字符串分割为子字符串。然后,将返回一个包含所有子字符串的数组,可以用 split() 方法将字符串分成单词、字符等。

const strText = "Hello China,I love China";
const words = strText.split(" ");
console.log(words); // [ 'Hello', 'China,I', 'love', 'China' ]

const chars = strText.split("");
console.log(chars); // ['H', 'e', 'l', 'l', 'o',' ', 'C', 'h', 'i', 'n','a', ',', 'I', ' ', 'l','o', 'v', 'e', ' ', 'C','h', 'i', 'n', 'a']

const strCopyText = strText.split();
console.log(strCopyText); // [ 'Hello China,I love China' ]

编写正则表达式模式

在 JavaScript 中,可以使用简单模式、特殊字符和标记来编写正则表达式模式。接下来将探索编写正则表达式的不同方法,重点关注简单模式、特殊字符和标记。

简单模式

有时,在搜索文本时,会希望获得完全匹配。例如,如果想搜索单词 China ,在 Hello China,I love China 这句话中搜索 China 。不会想要得到诸如 Chin 之类的结果,想要得到像 China 的精确匹配,这就首选简单模式。

const strText = "Hello China,I love China";
const regex = /China/;
console.log(strText.search(regex)); // 6

特殊字符

搜索有时不必精确,例如,可能想要使用范围进行搜索。可能想要搜索字母 a-c ,尽管字符串中它们之间是否有空格,为此,就需要使用特殊字符。JavaScript 中 RegExp 的特殊字符分为以下几类:断言、字符类、组和范围、量词和 Unicode 属性转义。接下来看看如何在这些类别中使用特殊字符。

断言

RegExp 中的断言表示模式边界,使用断言,可以指示单词的开头和结尾,还可以使用以下表达式为匹配编写模式:正向或者反向预查。 对于边界类型的断言,可以使用字符像 ^ 、$ 、\b 或 \B,语法如下:

  • ^ 匹配输入的开头。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 '\n' 或 '\r' 之后的位置。
  • $ 匹配输入的结尾。如果设置了 RegExp 对象的 Multiline 属性,$ 也匹配 '\n' 或 '\r' 之前的位置。
  • \b 匹配单词边界,也就是指单词和空格间的位置。例如,er\b 可以匹配 never 中的 er ,但不能匹配 verb 中的 er 。
  • \B 匹配非单词边界。如 er\B 能匹配 verb 中的 er ,但不能匹配 never 中的 er 。

对于正向或者反向预查的表达式,语法如下:

  • x(?=y) 正向肯定预查。仅当 x 后跟 y 时才会匹配 x 。将 x 和 y 替换选择的值以执行断言。例如,/Man(?=Money)/ 仅当后面跟有 money 时才匹配man
  • x(?!y) 正向否定预查。在任何不匹配 y 的字符串开始处匹配查找 x 。例如,/Man(?=Money)/ 仅当后面没有 money 时才匹配 man 。
  • (?<=y)x 反向肯定预查,与正向肯定预查类似,只是方向相反。如果前面有 y 才匹配 x 。例如,/Man(?=Money)/ 仅当前面有 money 时才会匹配 man 。
  • (?<!y)x 反向否定预查,与正向否定预查类似,只是方向相反。如果没有 y 才匹配 x。例如,/Man(?=Money)/ 仅当前面没有 money 时才会匹配 man 。

下面是特殊字符和断言的实例代码:

let str1 = `let the river dry up`;

// 1) 使用 ^ 将匹配固定在字符串的开头,换行符之后。
str1 = str1.replace(/^l/, "h");
console.log(1, str1); // 1 het the river dry up

// 2) 使用 $ 来修复字符串末尾和换行符之前的匹配。
let str2 = `let the river dry up`;
str2 = str2.replace(/p$/, "n");
console.log(2, str2); // 2 let the river dry un

// 3) 使用 \b 匹配单词边界
let str3 = `let the river dry up`;
str3 = str3.replace(/\bl/, "n");
console.log(3, str3); // 3 net the river dry up

// 4) 使用 \B 匹配非单词边界
let str4 = `let the river dry up`;
str4 = str4.replace(/\Bt/, "y");
console.log(4, str4); // 4 ley the river dry up

// 正向肯定预查,替换后面紧跟 make的us字符串,make 前面有空格
let str5 = "let us make light";
str5 = str5.replace(/us(?= make)/, "them");
console.log(5, str5); // 6 let everyone make light

// 正向否定预查,替换后面不是 let的us字符串
let str6 = "let us make light";
str6 = str6.replace(/us(?! let)/, "everyone");
console.log(6, str6); // 6 let everyone make light

let str6_2 = "let us let light";
str6_2 = str6_2.replace(/us(?! let)/, "everyone");
console.log("6.2", str6_2); // 6.2 let us let light

// 反向肯定预查,替换前面有let的us字符串
let str7 = "let us make light";
str7 = str7.replace(/(?<=let)us/, "them");
console.log(7, str7); // 7 let us make light

字符类

字符类用于区分不同的字符。例如,可以使用字符类区分字母和字母。来看看带有字符类的特殊字符以及它们是如何工作的。

  • \d 匹配一个数字字符,等价于 [0-9]
  • \D 匹配一个非数字字符,等价于 [^0-9]
  • \w 匹配字母、数字、下划线,等价于 [A-Za-z0-9_] 。
  • \W 匹配非字母数字字符。即不是来自基本拉丁字母的字符,等价于 [^A-Za-z0–9_]
  • \s 匹配任何空白字符,包括空格、制表符、换页符等等,等价于 [ \f\n\r\t\v]
  • . 匹配除换行符(\n、\r)之外的任何单个字符,要匹配包括 \n 在内的任何字符,请使用像 (.|\n) 的模式。
  • \xhh 匹配具有两个十六进制数字的字符。
  • \uhhhh 匹配带有十六进制数字的 UTF-16 代码单元。
  • \cX 用于使用插入符号匹配控制字符。

还有其他特殊字符,如\t 、\r 、 \n 、 \v 、\f ,分别匹配水平制表符、回车符、换行符、垂直制表符和换页符。

下面来看看实例代码:

const chess = "She played the Queen in C4 and he moved his King in c2.";
const coordinates = /\w\d/g;
console.log(chess.match(coordinates)); // [ 'C4', 'c2' ]

const mood = "happy ?, confused ?, sad ?";
const emoji = /[\u{1F600}-\u{1F64F}]/gu;
console.log(mood.match(emoji)); // [ '?', '?', '?' ]

分组和范围

如果想对表达式字符进行分组或指定范围,则可以使用特殊字符来完成此操作。

  • x|y 用于匹配 x 或 y 。例如,表达式 189|188 将匹配字符串中的 189 或者 188
  • [xbz] 用于匹配括号内的任何字符。例如,[xbz] 将匹配字符串中的 x 、 b、 z
  • [a-c] 用于匹配括号中字符范围内的任何字符。例如,[a-c] 将匹配 a 、 b 、 c 。但是,如果连字符位于括号的开头或结尾,则将其视为普通字符。如 [-ac] 将匹配 “非营利” 中的连字符。
  • [^xyz] 匹配未括在括号中的任何字符。例如,[^xyz] 不会匹配 Lazy 中的 y 和 z ,但会匹配 L 和 A
  • [^a-c] 匹配括号中包含的字符范围内不包含的任何内容。例如,[^a-c] 不会匹配 bank 中的 b 和 a,但会匹配 n 和 k 。
  • (x) 用于捕获组,例如,(x) 将匹配字符 x 并存储匹配的字符以供以后参考使用。例如 /(family)/ 在 make family familiar 中匹配并存储 family ,就像在捕获组中一样。
  • \n 用作最后一个子字符串的反向引用,匹配正则表达式中的组号 n ,其中 n 是正整数。
  • \k<Name> 对最后一个子字符串的反向引用,匹配由<Name>指定的已命名捕获组
  • (?<Name>x) 用于名称捕获组,匹配 x 并将其存储在返回匹配的组属性中,并使用 <Name> 指定的名称。
  • (?:x) 用于非捕获组,在这种情况下,模式匹配 x,但是它不存储匹配组,因此,无法从结果数组中回忆起匹配的子字符串。

捕获组就是把正则表达式中子表达式匹配的内容,保存到内存中以数字编号或显式命名的组里,方便后面引用。

有时候,因为特殊原因用到了(),但又没有引用它的必要,这时候就可以用非捕获组声明,防止它作为捕获组,降低内存浪费

let str1 = `let the river dry up`;
str1 = str1.replace(/let|the/, "m");
console.log(1, str1); // 1 m the river dry up

let str2 = `let the river dry up`;
str2 = str2.replace(/[abcde]/, "o");
console.log(2, str2); // 2 lot the river dry up

let str3 = `let the river dry up`;
str3 = str3.replace(/[^abcde]/, "o");
console.log(3, str3); // 3 oet the river dry up

let str4 = "Sir, yes Sir in Do you copy? Sir, yes Sir!";
str4 = str4.replace(/(?<title>\w+), yes \k<title>/, "Hello");
console.log(4, str4); // 4 Hello in Do you copy? Sir, yes Sir!

量词

匹配字符时,有时需要指定要匹配的表达式或字符的数量。量词可以指明想要匹配的表达式或字符的数量。

  • x* 用于匹配 x 零次或多次。例如,/bo*/ 匹配 bin Bird 和 nothing in goat
  • x+ 用于匹配 x 一次或多次。/x+/ 相当于 {1,}
  • x? 用于匹配 x 零次或一次。
  • x{n} 用于匹配 x n 次 ,n 为正整数。
  • x{n,} 用于匹配 x >=n 次,其中“n”是正整数。
  • x{n,m} 用于匹配 x n 到 m 次
let str = `let the river dry up`;
str = str.replace(/et*/, "a");
console.log(1, str); // 1 la the river dry up

let str1 = `let the river dry up`;
str1 = str1.replace(/e+/, "a");
console.log(2, str1); // 2 lat the river dry up

let str2 = `let the river dry up`;
str2 = str2.replace(/e?et?/, "a");
console.log(3, str2); // 3 la the river dry up

let str3 = `let the riveer dry up`;
str3 = str3.replace(/e{2}/, "a");
console.log(4, str3); // 4 let the rivar dry up

let str4 = `let the riveer dry up`;
str4 = str4.replace(/e{2,}/, "a");
console.log(5, str4); // 5 let the rivar dry up

let str5 = `let theee riveer dry up`;
str5 = str5.replace(/e{1,3}/, "a");
console.log(6, str5); // 6 lat theee riveer dry up

标记

JavaScript 中的正则表达式有5个常用的标记,这些标记可以增强正则表达式模式。

  • d 用于为子字符串匹配生成索引。
  • g 用于表示全局搜索。
  • i 用于表示不区分大小写的搜索。
  • m 用于多行匹配,使边界字符 ^ 和 $ 匹配每一行的开头和结尾,是多行,而不是整个字符串的开头和结尾。
  • s 特殊字符圆点 . 中包含换行符 \n,默认情况下的圆点 . 是匹配除换行符 \n 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 \n