第53课:字符串与正则表达式——模板字符串、match/replace、正则语法、RegExp 对象
字符串是文本的载体,正则表达式是模式匹配的利器。在表单验证、数据提取、搜索替换、日志分析等场景中,字符串方法与正则表达式密不可分。本节课将系统讲解 ES6 模板字符串的标签函数与高级用法、字符串的原型方法(特别是与正则配合的 match、replace、search、split),以及 JavaScript 正则表达式的完整语法——从元字符、量词、分组到断言——最后介绍 RegExp 构造函数和标志位的行为细节。
1. 模板字符串的进阶用法
1.1 多行字符串与表达式插值
模板字符串使用反引号 ` 定义,支持**跨行**和 ${} 表达式嵌入。
1 | const name = 'Alice'; |
所有 ${} 内的表达式在模板字符串定义时立即求值,并调用其 .toString() 方法转为字符串。
1.2 标签函数(Tagged Templates)
标签函数是模板字符串的高级特性:一个函数可以“解析”模板字符串的静态部分和动态插值,返回任意结果(不一定是字符串)。这对于构建安全的 HTML 模板、国际化、CSS-in-JS 等非常有用。
1 | function highlight(strings, ...values) { |
标签函数的第一个参数 strings 是模板字符串被插值分割后的静态字符串数组,它还有一个 .raw 属性,包含原始未转义的字符串(反斜杠保留)。
1 | function rawDemo(strings) { |
1.3 模板字符串用于 HTML 转义
利用标签函数可以构建防止 XSS 的 HTML 转义功能:
1 | function safeHtml(strings, ...values) { |
2. 正则表达式基础语法
正则表达式是一种描述字符模式的对象,用于匹配、搜索、替换字符串中的特定子串。
2.1 创建正则的两种方式
1 | // 字面量(编译时编译,性能更优,适合固定模式) |
当模式中包含变量时,只能使用构造函数:
1 | const searchTerm = 'hello'; |
注意:构造函数中,反斜杠需要双重转义。例如要匹配一个反斜杠 \,字面量写为 /\\/,构造函数写为 new RegExp('\\\\')。
2.2 标志位(Flags)
| 标志 | 名称 | 说明 |
|---|---|---|
i |
ignoreCase | 忽略大小写匹配。 |
g |
global | 全局匹配(查找所有匹配项,而非在第一个匹配后停止)。 |
m |
multiline | 多行模式:^ 和 $ 匹配一行的开始和结束,而非整个字符串。 |
s |
dotAll (ES2018) | 允许 . 匹配换行符 \n。 |
u |
unicode (ES6) | 开启 Unicode 支持(正确处理代理对,支持 \u{...} 语法)。 |
y |
sticky | 粘性匹配:仅从 lastIndex 指示的位置开始匹配,不向前搜索。 |
2.3 元字符速览
| 字符/模式 | 含义 |
|---|---|
. |
匹配任意单个字符(默认不含换行,s 标志下含换行)。 |
\d |
数字 [0-9]。\D 为非数字。 |
\w |
单词字符 [a-zA-Z0-9_]。\W 为非单词字符。 |
\s |
空白字符(空格、制表、换行等)。\S 为非空白。 |
\b |
单词边界(单词与空格或标点之间)。\B 为非单词边界。 |
^ |
匹配字符串/行的开始。 |
$ |
匹配字符串/行的结束。 |
[abc] |
字符集:匹配 a、b 或 c 中任意一个。 |
[^abc] |
反向字符集:匹配不在 a、b、c 中的任意字符。 |
| `a | b` |
(pattern) |
捕获组:捕获匹配的子串,后续可通过 \1 引用或 match 返回。 |
2.4 量词
| 量词 | 含义 |
|---|---|
* |
0 次或多次(贪婪)。 |
+ |
1 次或多次(贪婪)。 |
? |
0 次或 1 次(可选)。 |
{n} |
恰好 n 次。 |
{n,} |
至少 n 次。 |
{n,m} |
至少 n 次,至多 m 次。 |
贪婪与懒惰:默认量词是贪婪的(尽可能多地匹配)。在量词后加 ? 变为懒惰(尽可能少地匹配)。
1 | const str = '<div>content</div>'; |
2.5 捕获组、非捕获组与反向引用
| 语法 | 含义 |
|---|---|
(pattern) |
捕获组:匹配并记住子串,可在结果和替换中引用。 |
(?:pattern) |
非捕获组:仅分组,不记住匹配结果(性能更好)。 |
(?<name>...) |
命名捕获组(ES2018):给组命名,通过 groups.name 访问。 |
\1、\2 |
反向引用:在正则内部引用第 1、第 2 个捕获组匹配到的文本。 |
1 | // 捕获组示例:提取日期各部分 |
2.6 环视断言(Lookaround)
断言检查当前位置前后是否符合某条件,但不消耗字符(不包含在匹配结果中)。
| 语法 | 名称 | 含义 |
|---|---|---|
(?=pattern) |
正向前瞻 | 后面必须紧跟着 pattern。 |
(?!pattern) |
负向前瞻 | 后面不能紧跟着 pattern。 |
(?<=pattern) |
正向后顾(ES2018) | 前面必须紧跟着 pattern。 |
(?<!pattern) |
负向后顾(ES2018) | 前面不能紧跟着 pattern。 |
1 | // 正向前瞻:匹配后面是数字的字母 |
3. 字符串与正则配合的核心方法
3.1 match
str.match(regex) 根据正则匹配字符串。
- 无
g标志:返回第一个匹配结果的信息数组(包含捕获组),无匹配返回null。 - 有
g标志:返回所有匹配子串组成的数组(无捕获组详情),无匹配返回null。
1 | const str = 'The quick brown fox jumps over the lazy dog.'; |
3.2 matchAll(ES2020)
返回一个迭代器,每次迭代产生一个包含完整匹配结果和捕获组的数组(包含 groups、index 等属性)。适用于需要获取全局匹配的完整信息。
1 | const regex = /(?<word>\b\w{4}\b)/g; |
3.3 replace 与 replaceAll
str.replace(regex|substr, newSubstr|function) 替换匹配项。返回新字符串,原字符串不变。
- 字符串替换:仅替换第一个匹配。
- 正则替换:搭配
g标志替换所有匹配。ES2021 的replaceAll可用字符串进行全局替换。
1 | const text = 'apple banana apple grape'; |
替换函数:第二个参数可以是回调函数,其参数依次为 match, p1, p2, ..., offset, string。用于动态替换。
1 | const prices = '10 USD, 20 EUR, 30 GBP'; |
3.4 search
str.search(regex) 返回第一个匹配的索引,未找到返回 -1。它忽略 g 标志,且总是从字符串开始位置搜索(不更新 lastIndex)。
1 | console.log('hello world'.search(/\bworld\b/)); // 6 |
3.5 split
str.split(separator, limit?) 按分隔符(字符串或正则)将字符串拆分为数组。当分隔符为正则时,捕获组的内容会被包含在结果数组中。
1 | console.log('a1b2c3'.split(/\d/)); // ['a', 'b', 'c', ''] |
4. RegExp 对象与动态模式
4.1 构造函数与标志位的动态传递
1 | const pattern = 'hello'; |
当需要根据用户输入动态构建正则时,必须对特殊字符进行转义,防止注入错误的正则语法或导致匹配逻辑异常。
1 | function escapeRegExp(string) { |
4.2 实例属性与 lastIndex
RegExp 实例有以下常用属性:
| 属性 | 说明 |
|---|---|
source |
正则表达式的模式字符串(不含标志)。 |
flags |
标志位字符串,如 'gi'。 |
lastIndex |
下一次匹配开始的索引(仅对 g 或 y 标志有效)。 |
dotAll |
是否设置了 s 标志。 |
unicode |
是否设置了 u 标志。 |
sticky |
是否设置了 y 标志。 |
当使用 g 或 y 标志时,同一个正则表达式在多次调用 exec 或 test 时会维护 lastIndex,每次匹配后该值会更新到下次匹配的起始位置。这是极易出错的点。
1 | const regex = /foo/g; |
如果在未重置的情况下复用同一个正则对象,会得到不符合预期的结果。解决方法是每次都创建新的正则实例,或在每次循环前手动设置 regex.lastIndex = 0。
5. 常见正则表达式实战
5.1 邮箱格式校验
1 | const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; |
5.2 密码强度校验(至少包含大小写字母和数字)
1 | const strongPwd = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/; |
5.3 从 URL 中提取参数
1 | const url = 'https://example.com?page=2&size=10&sort=desc'; |
课后练习
一、概念自测(选择题 / 填空题)
(单选) 正则表达式
/a+/g中,+表示什么含义?
A. 匹配字母 a,0 次或 1 次
B. 匹配字母 a,1 次或多次(贪婪)
C. 匹配字母 a,0 次或多次(贪婪)
D. 匹配字母 a,恰好 1 次(单选) 使用
str.replace(regex, replacement)进行全局替换,正则表达式必须包含哪个标志?
A.i
B.m
C.g
D.u(填空) 在正则表达式中,
(?<=prefix)pattern被称为______断言(正向后顾),它要求匹配位置前面必须有 prefix,但 prefix 不包含在匹配结果中。(多选) 以下哪些字符串方法可以接受正则表达式作为参数?
A.match
B.replace
C.split
D.search
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要编写一个 JavaScript 函数 formatPhoneNumber(phone),它接收一个包含数字的字符串(可能包含空格、破折号、括号等非数字字符),并返回格式为 (XXX) XXX-XXXX 的美国电话号码格式。如果输入字符串中数字不足 10 位,返回原始输入字符串。要求如下:
- 使用正则表达式提取所有数字。
- 判断提取的数字长度是否为 10,若不是则返回原字符串。
- 使用捕获组和替换函数将数字重新格式化为
(xxx) xxx-xxxx(可使用replace+ 正则,或直接拼接)。 - 使用
const声明函数。 - 提供多个测试用例。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 JavaScript 代码。提示词中需明确指定正则的用法(提取数字、格式化替换)、边界情况处理。
三、面试真题与参考答案
题目(百度前端面试题):
请解释正则表达式的“贪婪匹配”与“懒惰匹配”的区别。给定字符串
'<div>hello</div><span>world</span>',如果使用正则/<.*>/和/<.*?>/去匹配,各会得到什么结果?为什么?
参考答案:
- 贪婪匹配:量词默认尽可能多地匹配。
/<.*>/中的.*会一直匹配到字符串的最后一个字符,然后因为>无法匹配末尾而逐步回退,直到找到最后一个>。对于给定字符串,它匹配整个'<div>hello</div><span>world</span>'。 - 懒惰匹配:量词后加
?变为懒惰,尽可能少地匹配。/<.*?>/中的.*?匹配到第一个>即停止。对于给定字符串,它会依次匹配'<div>'、'</div>'、'<span>'、'</span>'(如果用全局匹配g则会得到四个结果;无g时只返回第一个'<div>')。 - 原因:正则引擎在贪婪模式下会优先匹配尽量长的子串,仅在后续部分无法匹配时才回退;懒惰模式下优先匹配最短的子串,若后续无法匹配才逐步增加。
课后练习答案
一、概念自测答案
B
- 解析:
+表示 1 次或多次,默认贪婪。A 描述的是?,C 描述的是*。
- 解析:
C
- 解析:
g标志(global)使replace替换所有匹配项,而非仅第一个。
- 解析:
正向后顾(或 正向回顾后发)
- 解析:
(?<=...)是正向后顾断言,ES2018 引入。
- 解析:
A、B、C、D
- 解析:
match、replace、split、search均可接受正则表达式作为参数。
- 解析:
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请编写一个 JavaScript 函数formatPhoneNumber(phone),将包含任意格式的美国电话号码转换为(XXX) XXX-XXXX格式。要求:
- 使用
const声明函数。- 先用正则
/\d/g提取字符串中的所有数字,若长度 !== 10 则直接返回原字符串。- 将 10 位数字分组:前 3 位区号,中间 3 位,后 4 位。使用正则
/(\d{3})(\d{3})(\d{4})/的replace方法替换为'($1) $2-$3'。- 添加 JSDoc 注释。
- 提供测试用例注释:
'123-456-7890'→'(123) 456-7890'、'(987)654-3210'→'(987) 654-3210'、'abc'→'abc'。- 直接输出完整代码。”