目录
一、介绍
ECMAScript 正则表达式是 JavaScript 中用于匹配和操作字符串的一种强大工具。它基于正则表达式语法,提供了一种灵活、高效的方式来进行模式匹配和字符串处理。
下面是一个转换 css 格式的示例:
function toCamelCase(str) {
return str.replace(/-([a-z])/g, function(match, p1) {
return p1.toUpperCase();
});
}
// 测试样例
console.log(toCamelCase('font-size')); // 输出:'fontSize'
console.log(toCamelCase('-webkit-border-image')); // 输出:'webkitBorderImage'
以下是 ECMAScript 正则表达式的一些常用特性:
-
基本匹配:使用正则表达式可以进行基本的字符串匹配,例如查找某个特定的模式或字符。
-
字符类:通过字符类可以匹配一组字符中的任意一个。例如,
[a-z]
可以匹配任意小写字母。 -
量词:量词用于指定匹配的次数,例如
*
匹配零个或多个,+
匹配一个或多个,?
匹配零个或一个。 -
捕获组:使用圆括号
()
可以创建捕获组,用于提取匹配的子字符串。另外捕获组可以通过反向引用在同一个正则表达式中进行后向引用。 -
边界匹配:使用
^
和$
可以匹配字符串的开头和结尾,\b
和\B
可以匹配单词边界。 -
特殊字符:正则表达式中有一些特殊字符,例如
.
匹配任意字符,\d
匹配数字,\w
匹配单词字符等。 -
修饰符:修饰符用于指定匹配的方式,例如
g
表示全局匹配,i
表示不区分大小写,m
表示多行匹配等。
ECMAScript 正则表达式在 JavaScript 中广泛应用于字符串处理、表单验证、文本搜索等场景中,是 JavaScript 中非常重要的特性之一。
二、提供的 API
ECMAScript 提供了一系列用于处理正则表达式的 API,这些 API 可以在 JavaScript 中进行字符串的匹配、搜索、替换等操作。
以下是一些主要的 API:
1. RegExp 对象
RegExp 对象是 JavaScript 中用于表示正则表达式的对象。
通过构造函数 RegExp()
或直接使用字面量 /pattern/flags
创建正则表达式对象,然后可以调用其方法进行匹配和搜索操作。
2. String 对象的正则表达式方法
String 对象提供了多个方法用于正则表达式的匹配和搜索,包括 match()
、search()
、replace()
、split()
等。其中 replace
相当常用。
这些 API 提供了丰富的功能,可以满足 JavaScript 中对于正则表达式的各种需求。
三、RegExp 对象
RegExp 对象是 JavaScript 中用于表示正则表达式的对象。
通过 RegExp 构造函数或直接使用正则表达式字面量来创建 RegExp 对象。
RegExp 对象具有以下常用属性和方法:
属性:
global
:一个布尔值,表示是否启用全局匹配,默认为 false。ignoreCase
:一个布尔值,表示是否忽略大小写,默认为 false。multiline
:一个布尔值,表示是否启用多行模式,默认为 false。source
:一个只读的字符串,包含正则表达式的文本。lastIndex
:一个整数,表示下一次匹配的开始位置,默认为 0。
方法:
exec()
:在字符串中执行匹配搜索,返回匹配结果的数组或 null。test()
:测试字符串是否匹配正则表达式,返回 true 或 false。
以下是一些示例代码,演示了如何使用 RegExp 对象:
// 使用 RegExp 构造函数创建正则表达式对象
const pattern1 = new RegExp('hello', 'i'); // 忽略大小写匹配
const pattern2 = new RegExp('\\d+', 'g'); // 全局匹配数字
console.log(pattern1.test('Hello, world!')); // 输出:true
console.log(pattern2.exec('123abc456def')); // 输出:['123']
// 使用正则表达式字面量创建正则表达式对象
const pattern3 = /world/; // 等价于 new RegExp('world')
console.log(pattern3.source); // 输出:'world'
console.log(pattern3.test('Hello, world!')); // 输出:true
在这个示例中,我们创建了几个不同的 RegExp 对象,并演示了如何使用它们进行字符串匹配和搜索。
RegExp 对象是 JavaScript 中处理正则表达式的重要工具之一,可以帮助我们进行灵活、高效的字符串处理。
1. pattern:模式字符串
在使用 new RegExp()
构造函数创建 RegExp 对象时,pattern
参数表示正则表达式的模式字符串,即要匹配的模式。这个模式字符串可以包含普通字符和特殊字符,用于描述匹配规则。
例如,如果我们想要匹配一个字符串中的所有数字,可以使用 \d+
作为模式字符串,其中 \d
表示匹配任意数字,+
表示匹配前面的字符一次或多次。又如,如果我们想要匹配一个字符串中的所有单词,可以使用 \w+
作为模式字符串,其中 \w
表示匹配任意单词字符,+
表示匹配前面的字符一次或多次。
2. flags:标志符
在使用 new RegExp()
构造函数创建 RegExp 对象时,flags
参数用于指定正则表达式的标志,以控制匹配的方式。
这些标志可以是以下任意组合:
g
:全局匹配,即匹配目标字符串中的所有匹配项,而不仅仅是第一个匹配项。i
:不区分大小写匹配。m
:多行匹配,在这种模式下,^
和$
匹配目标字符串的每行的开始和结束。
以下是一些示例,演示了如何使用 new RegExp()
构造函数创建 RegExp 对象,其中 flags
参数表示正则表达式的标志:
// 创建一个不区分大小写的正则表达式对象
const pattern1 = new RegExp('hello', 'i');
console.log(pattern1.test('Hello, world!')); // 输出:true
// 创建一个全局匹配的正则表达式对象
const pattern2 = new RegExp('\\d+', 'g');
console.log('123abc456def'.match(pattern2)); // 输出:['123', '456']
// 创建一个多行匹配的正则表达式对象
const str = `1
2
3`;
const pattern = /^\d+/gm;
const matches = str.match(pattern);
console.log(matches); // 输出:// ['1', '2', '3']
// const pattern = /^\d+/g; 仅输出 ['1']
在这些示例中,我们使用了不同的标志来创建 RegExp 对象,从而控制了匹配的方式。这些标志可以根据实际需要进行组合使用,以满足对字符串的不同匹配需求。
关于更多 flags 可以参考阅读 ecma262 官方文档。
四、二次转译问题
1. 构造函数 pattern 字符串
在前面的示例,其实可以发现一个问题:
// 构造函数
new RegExp('\\d+', 'g');
// 字面量
const pattern = /^\d+/gm;
同样的规则,为什么在构造函数里需要多一个反斜杠?
注意,RegExp 构造函数的两个参数都是字符串。模式字符串 pattern
参数可能需要进行二次转译,特别是在字符串中包含特殊字符或者需要转义的字符时。
由于在 JavaScript 中字符串字面量本身就需要进行转义,因此当你想要使用一个特殊字符或者需要转义的字符时,你可能需要在字符串中进行额外的转义,以确保正则表达式的模式字符串正确解析。
举个例子,假设我们想要匹配一个包含括号的字符串 “(example)”,我们需要使用正则表达式 \(
来匹配左括号,但是由于在字符串字面量中左括号也是一个特殊字符,因此我们需要在字符串中进行二次转译,使用两个反斜杠 \\(
来表示一个反斜杠和一个左括号。
以下是一个示例,演示了在模式字符串中包含特殊字符时进行二次转译:
// 匹配包含括号的字符串 "(example)"
const pattern = new RegExp('\\(example\\)');
console.log(pattern.test('(example)')); // 输出:true
console.log(pattern.test('example')); // 输出:false
在这个示例中,我们使用了模式字符串 \\(example\\)
来创建了一个 RegExp 对象,用于匹配包含括号的字符串。在字符串字面量中,\\(
表示一个反斜杠和一个左括号,同理 \\)
表示一个反斜杠和一个右括号,确保了正则表达式的模式字符串正确解析。
因此,在日常使用中,比较推荐用字面量。
2. 字面量元字符
在正则表达式的字面量中,有些特殊字符被用作元字符,它们具有特定的含义,例如 ^
、$
、.
、*
、+
、?
、\
、(
、)
、[
、]
、{
、}
、|
等。如果你想要匹配字符串中的这些字符本身而不是它们的特殊含义,就需要进行转义。
在正则表达式的字面量中,你可以使用反斜杠 \
进行转义,将特殊字符转换为普通字符。
以下是一个示例,演示了在正则表达式字面量中转义元字符的使用:
// 匹配字符串中的点号 "."
const dotPattern = /\./;
console.log(dotPattern.test('example.')); // 输出:true
// 匹配字符串中的反斜杠 "\"
const backslashPattern = /\\/;
console.log(backslashPattern.test('example\\')); // 输出:true
在这个示例中,我们使用了正则表达式字面量创建了两个 RegExp 对象,分别用于匹配字符串中的点号和反斜杠。在正则表达式字面量中,点号 .
和反斜杠 \
都是特殊字符,因此我们使用了反斜杠进行转义,以匹配字符串中的这两个字符本身。
五、特性
5.1 字符类
在 ECMAScript(JavaScript)的正则表达式中,字符类用于匹配一个字符集中的任何一个字符。
以下是一些常用的字符类:
\d
:匹配任何数字字符,等价于[0-9]
。\D
:匹配任何非数字字符,等价于[^0-9]
。\w
:匹配任何单词字符(字母、数字、下划线),等价于[A-Za-z0-9_]
。\W
:匹配任何非单词字符,等价于[^A-Za-z0-9_]
。\s
:匹配任何空白字符(空格、制表符、换行符等)。\S
:匹配任何非空白字符。.
:匹配除换行符之外的任何字符。
除了上述常用的字符类外,还可以使用自定义字符类,例如 [aeiou]
匹配任何元音字母,[^aeiou]
匹配任何非元音字母等。
以下是一个示例,演示了如何在正则表达式中使用字符类:
const str = 'a1 b2 c3';
const pattern1 = /\d/; // 匹配任何数字字符
const pattern2 = /\D/; // 匹配任何非数字字符
const pattern3 = /\w/; // 匹配任何单词字符
const pattern4 = /\W/; // 匹配任何非单词字符
const pattern5 = /\s/; // 匹配任何空白字符
const pattern6 = /\S/; // 匹配任何非空白字符
const pattern7 = /./; // 匹配除换行符之外的任何字符
console.log(pattern1.test(str)); // 输出:true
console.log(pattern2.test(str)); // 输出:true
console.log(pattern3.test(str)); // 输出:true
console.log(pattern4.test(str)); // 输出:false
console.log(pattern5.test(str)); // 输出:true
console.log(pattern6.test(str)); // 输出:true
console.log(pattern7.test(str)); // 输出:true
在这个示例中,我们使用了不同的字符类来创建正则表达式模式,并对字符串进行匹配。通过使用字符类,我们可以更方便地匹配特定类型的字符。
5.2 量词
在 ECMAScript(JavaScript)的正则表达式中,量词用于指定匹配的次数,可以控制一个模式的重复次数。
以下是一些常用的量词:
?
:匹配前面的模式零次或一次,相当于 {0,1}。*
:匹配前面的模式零次或多次,相当于 {0,}。+
:匹配前面的模式一次或多次,相当于 {1,}。{n}
:匹配前面的模式恰好 n 次。{n,}
:匹配前面的模式至少 n 次。{n,m}
:匹配前面的模式至少 n 次,但不超过 m 次。
除了上述常用的量词外,还有一些特殊的量词,例如 *?
、+?
、??
等,它们表示非贪婪或懒惰匹配,即尽可能少地匹配字符。
关于贪婪和非贪婪,在系列(二)会详细介绍。
以下是一个示例,演示了如何在正则表达式中使用量词:
const str = 'abcde';
const pattern1 = /ab?/; // 匹配 'a' 后跟零个或一个 'b'
const pattern2 = /ab*/; // 匹配 'a' 后跟零个或多个 'b'
const pattern3 = /ab+/; // 匹配 'a' 后跟一个或多个 'b'
const pattern4 = /ab{2}/; // 匹配 'a' 后跟两个 'b'
const pattern5 = /ab{2,}/; // 匹配 'a' 后跟至少两个 'b'
const pattern6 = /ab{1,3}/; // 匹配 'a' 后跟一个到三个 'b'
console.log(pattern1.test(str)); // 输出:true
console.log(pattern2.test(str)); // 输出:true
console.log(pattern3.test(str)); // 输出:true
console.log(pattern4.test(str)); // 输出:false
console.log(pattern5.test(str)); // 输出:false
console.log(pattern6.test(str)); // 输出:true
在这个示例中,我们使用了不同的量词来创建正则表达式模式,并对字符串进行匹配。通过使用量词,我们可以指定模式的重复次数,从而更精确地匹配目标字符串。
5.3 捕获组
在 ECMAScript(JavaScript)的正则表达式中,捕获组是用括号 ()
括起来的子表达式,它可以在匹配成功后提取匹配到的子字符串。
捕获组允许我们在正则表达式中指定一个子模式,然后在匹配成功后从匹配结果中提取出这个子模式所匹配到的内容。
以下是一个示例,演示了如何在正则表达式中使用捕获组:
const str = 'Hello, world!';
// 匹配以 'Hello, ' 开头,后跟任意字符的子字符串
// 使用捕获组提取后面的子字符串
const pattern = /(Hello, )(.*)/;
const match = str.match(pattern);
if (match) {
console.log('匹配结果:', match[0]); // 输出整个匹配结果
console.log('捕获组1:', match[1]); // 输出第一个捕获组
console.log('捕获组2:', match[2]); // 输出第二个捕获组
}
// 匹配结果: Hello, world!
// 捕获组1: Hello,
// 捕获组2: world!
在这个示例中,我们使用了正则表达式 /^(Hello, )(.*)/
,其中 (Hello, )
和 (.*)
分别是两个捕获组。当正则表达式与目标字符串匹配成功后,我们可以通过 match
数组获取到匹配结果和捕获组的内容。match[0]
表示整个匹配结果,而 match[1]
和 match[2]
分别表示第一个和第二个捕获组的内容。
1. 用途
捕获组在正则表达式中具有多种用途,主要包括以下几点:
- 提取匹配内容: 使用捕获组可以从匹配结果中提取出指定的子字符串,这对于从复杂的文本中提取特定信息非常有用。例如,从日期字符串中提取年、月、日等信息。
// 把一个日期格式为 2019-11-10 的年、月、日分别匹配出来
let text = "2019-11-10";
let pattern = /\d{4}-\d{2}-\d{2}/
let matchs = pattern.exec(text);
// [
// "2019-11-10"
// ]
// 没有使用捕获组的时候,匹配的是整个字符串,没有把年月日的信息分开匹配
let pattern = /(\d{4})-(\d{2})-(\d{2})/
let matchs = pattern.exec(text);
// [
// "2019-11-10",
// "2019",
// "11",
// "10"
// ]
- 分组匹配: 捕获组允许将多个子模式组合成一个整体,从而形成更复杂的模式。这样可以提高正则表达式的灵活性和可读性。例如,可以使用捕获组匹配电话号码的不同部分(区号、号码等)。
const phoneNumber = 'Tel: (123) 456-7890';
// 匹配电话号码的正则表达式
const pattern = /\((\d{3})\) (\d{3})-(\d{4})/;
// 执行匹配,并提取捕获组内容
const match = pattern.exec(phoneNumber);
if (match) {
const areaCode = match[1]; // 捕获组1:区号
const firstPart = match[2]; // 捕获组2:号码的第一部分
const secondPart = match[3]; // 捕获组3:号码的第二部分
console.log('区号:', areaCode);
console.log('号码:', `${firstPart}-${secondPart}`);
}
- 替换操作: 在替换操作中,捕获组可以用来引用匹配到的子字符串。通过在替换字符串中使用
$1
、$2
等语法,可以引用对应的捕获组内容,从而实现更精确的替换操作。
const phoneNumber = 'Tel: (123) 456-7890';
// 匹配电话号码的正则表达式
const pattern = /\((\d{3})\) (\d{3})-(\d{4})/;
// 替换操作,将电话号码中的区号和号码部分交换位置
const replacedPhoneNumber = phoneNumber.replace(pattern, '($3) $1-$2');
console.log('替换后的电话号码:', replacedPhoneNumber);
// 替换后的电话号码: Tel: (7890) 123-456
- 条件匹配: 捕获组还可以用于实现条件匹配,即根据捕获组是否匹配到内容来确定整个模式是否匹配成功。这可以通过使用逻辑运算符和捕获组的非捕获形式
(?:...)
来实现。
通常情况下,我们可以使用 |
运算符在正则表达式中创建多个选择项,其中每个选择项都包含一个不同的模式,如const pattern = /cat|dog|bird/;
。但是有时候我们希望根据某个特定的条件来确定是否匹配某个模式,这时就可以使用捕获组和非捕获形式来实现条件匹配。
以下是一个示例,演示了如何在正则表达式中使用捕获组和非捕获形式来实现条件匹配:
// 匹配邮箱地址
// 邮箱地址的用户名部分可以包含字母、数字、下划线和连字符,但不能以连字符开头或结尾
// 邮箱地址的域名部分可以包含字母、数字和连字符,但不能以连字符开头或结尾
const pattern = /^([a-zA-Z0-9_](?:[a-zA-Z0-9_-]*[a-zA-Z0-9_])?)@([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)?)$/;
// 测试匹配结果
console.log(pattern.test('user@example.com')); // 输出:true
console.log(pattern.test('user123@example.com')); // 输出:true
console.log(pattern.test('user-123@example.com')); // 输出:true
console.log(pattern.test('-user@example.com')); // 输出:false
console.log(pattern.test('user@-example.com')); // 输出:false
在这个示例中,我们使用了捕获组和非捕获形式来实现对邮箱地址的条件匹配。正则表达式 /^([a-zA-Z0-9_](?:[a-zA-Z0-9_-]*[a-zA-Z0-9_])?)@([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)?)$/
包含了两个捕获组,分别匹配邮箱地址的用户名部分和域名部分。同时,我们使用了非捕获形式 (?:...)
来确保用户名和域名部分不以连字符开头或结尾。这样就可以根据捕获组的匹配情况来确定整个邮箱地址是否匹配成功。
2. 捕获组和非捕获组
捕获组和非捕获组都是正则表达式中的分组结构,用于对模式进行分组。它们的主要区别在于是否会在匹配结果中保存分组的内容。
-
捕获组(Capturing Group): 使用圆括号
()
来创建捕获组,用于对模式中的一部分进行分组,并将匹配到的内容保存在匹配结果中。捕获组可以通过索引或者特殊变量来引用。例如,在替换操作中,可以使用$1
、$2
等语法来引用捕获组的内容。示例:/(ab)c/
中的(ab)
是一个捕获组,它匹配字符串中的 “abc”,并将 “ab” 保存在捕获组中。 -
非捕获组(Non-capturing Group): 使用
(?:)
来创建非捕获组,也是用于对模式中的一部分进行分组,但不会将匹配到的内容保存在匹配结果中。非捕获组不会分配索引,也不会影响后续的引用。它的主要作用是提高正则表达式的效率和可读性。示例:/(?:ab)c/
中的(?:ab)
是一个非捕获组,它匹配字符串中的 “abc”,但不会将 “ab” 保存在匹配结果中。
非捕获组在正则表达式中的使用场景有几个:
-
提高效率: 非捕获组不会保存匹配内容,因此在匹配过程中不会消耗额外的内存来存储捕获组的内容。在大部分情况下,我们只需要匹配模式而不需要保存捕获组的内容,这时就可以使用非捕获组来提高匹配的效率。
-
避免混淆: 当正则表达式中存在多个捕获组时,可能会导致匹配结果的混淆,尤其是在使用
exec()
或match()
等方法返回的匹配结果数组中。非捕获组可以避免这种混淆,因为它不会保存匹配内容,所以在匹配结果中不会出现非捕获组的内容,这样可以让匹配结果更清晰。 -
逻辑分组: 在一些复杂的正则表达式中,可能需要使用分组来指定模式的逻辑结构,但又不希望保存捕获组的内容。非捕获组可以用来实现这种逻辑分组,从而使正则表达式的结构更清晰。
总的来说,当我们只需要匹配模式而不需要保存捕获组的内容,或者希望避免匹配结果的混淆时,可以使用非捕获组来提高效率和可读性。
3. 反向引用
捕获组可以通过反向引用在同一个正则表达式中进行后向引用。后向引用允许在正则表达式中引用前面匹配到的捕获组内容,并将其作为后续模式的一部分进行匹配。
反向引用使用 \
加上捕获组的编号来表示,捕获组的编号从左括号开始计数。例如,\1
表示对第一个捕获组的引用,\2
表示对第二个捕获组的引用,依此类推。
以下是一个示例,演示了在同一个正则表达式中如何使用捕获组进行后向引用:
const str = "abc abc";
const pattern = /(\w+) \1/;
// 匹配一个单词,后跟一个空格,然后再次匹配之前匹配到的单词
const match = str.match(pattern);
if (match) {
console.log('匹配结果:', match[0]); // 输出:'abc abc'
}
在这个示例中,正则表达式 (\w+) \1
中的 \1
表示对第一个捕获组 (\w+)
的反向引用。它会匹配到一个单词,然后匹配一个空格,接着再次匹配之前匹配到的单词。因此,整个正则表达式成功匹配了连续出现的相同单词。
另一个示例,匹配如 1212,3434,7979
这类数据。可以看出,后两个数据是前两个数据的重复,那就是ABAB这种模式,所以我们可以把AB先分组,然后在对其引用就可以了。
在正则表达式中,你可以使用反向引用来引用之前捕获的组。反向引用的语法是使用 \
加上组的编号。具体来说,你可以使用 \1
、\2
、\3
等来引用第一个、第二个、第三个捕获的组。
即(\d{2})\1
,把\d{2}
匹配到的内容先保存到捕获组1中,然后再对捕获组1进行引用,当(\d{2})
匹配到的是12的时候,\1
就表示12,当(\d{2})
匹配到的是34的时候,\1
就是34。
let text = "1212,3434,7979";
let pattern = /(\d{2})\1/gi;
let matchs = pattern.exec(text);
// [
// "1212",
// "12"
// ]
matchs = pattern.exec(text);
// [
// "3434",
// "34"
// ]
六、正则表达式的 toString() 和 valueOf()
1. toString() 方法
正则表达式对象的 toString()
方法返回一个表示正则表达式字面量的字符串。它返回的字符串包含正则表达式的模式和标志。
注意,无论是构造函数还是字面量都会返回字面量字符串。
示例:
const regex = /\d+/g;
console.log(regex.toString()); // 输出: "/\d+/g"
const pattern = new RegExp('hello', 'i');
console.log(pattern.toString()); // 输出: '/hello/i'
2. valueOf() 方法
正则表达式对象的 valueOf()
方法返回正则表达式对象本身。通常情况下,它返回的是正则表达式对象的一个引用。
通常情况下,我们更倾向于使用 toString()
方法来获取正则表达式的字符串表示形式,因为它返回的字符串更加直观。valueOf()
方法通常用于在需要获取对象本身的情况下,例如进行比较或者类型转换等。