JavaScript正则表达式
正则表达式(Regular Expression,简称RegExp)是一种强大的文本模式匹配和搜索工具,在JavaScript中被广泛应用于字符串处理。本文将详细介绍JavaScript中正则表达式的语法、用法和实践技巧。
正则表达式基础
什么是正则表达式
正则表达式是由一系列字符和特殊符号组成的模式,用于描述和匹配文本中的字符组合。它可以用于:
- 搜索文本中的特定模式
- 验证输入(如邮箱、电话号码等)
- 替换文本中的特定部分
- 从文本中提取信息
创建正则表达式
在JavaScript中,有两种方式创建正则表达式:
// 字面量语法
const regex1 = /pattern/flags;
// 构造函数语法
const regex2 = new RegExp("pattern", "flags");
字面量语法更简洁,而构造函数语法允许在运行时动态构建正则表达式:
// 字面量语法示例
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
// 构造函数语法示例
const searchTerm = "JavaScript";
const dynamicRegex = new RegExp(searchTerm, "i");
正则表达式标志
标志(flags)用于修改正则表达式的匹配行为:
// 常用标志
const regex = /pattern/igmsuyd;
标志 | 描述 |
---|---|
g | 全局匹配 - 查找所有匹配项,而不是在第一个匹配后停止 |
i | 忽略大小写 - 使匹配不区分大小写 |
m | 多行匹配 - 使^ 和$ 匹配每一行的开头和结尾 |
s | 点号匹配所有 - 使. 匹配换行符(ES2018) |
u | Unicode模式 - 启用Unicode匹配(ES6) |
y | 粘性匹配 - 仅匹配正则表达式的lastIndex 属性指定的索引处(ES6) |
d | 生成匹配索引 - 使匹配结果包含开始和结束位置(ES2022) |
// 示例
const text = "Hello World\nHello JavaScript";
// 不使用m标志,只匹配整个字符串的开头
console.log(/^Hello/.test(text)); // true
console.log(/^JavaScript/.test(text)); // false
// 使用m标志,匹配每一行的开头
console.log(/^Hello/m.test(text)); // true
console.log(/^JavaScript/m.test(text)); // true
正则表达式模式
基本字符匹配
最简单的正则表达式是直接匹配字符:
const regex = /hello/;
console.log(regex.test("hello world")); // true
console.log(regex.test("Hello world")); // false(区分大小写)
特殊字符
某些字符在正则表达式中有特殊含义:
// 需要使用反斜杠转义的特殊字符
// . * + ? ^ $ \ | ( ) [ ] { }
const regex = /\$\d+\.\d{2}/; // 匹配金额,如$10.99
console.log(regex.test("The price is $10.99")); // true
字符类
字符类用方括号[]
表示,匹配括号内的任意一个字符:
// 匹配a、b或c
const regex1 = /[abc]/;
console.log(regex1.test("apple")); // true
console.log(regex1.test("dog")); // false
// 范围表示法
const regex2 = /[a-z]/; // 匹配任何小写字母
const regex3 = /[A-Z]/; // 匹配任何大写字母
const regex4 = /[0-9]/; // 匹配任何数字
const regex5 = /[a-zA-Z0-9]/; // 匹配任何字母或数字
// 否定字符类(匹配除括号内字符以外的任何字符)
const regex6 = /[^0-9]/; // 匹配任何非数字字符
console.log(regex6.test("123")); // false
console.log(regex6.test("a123")); // true
预定义字符类
JavaScript提供了一些常用字符类的简写:
// 常用预定义字符类
const regex1 = /\d/; // 等同于[0-9],匹配任何数字
const regex2 = /\D/; // 等同于[^0-9],匹配任何非数字字符
const regex3 = /\w/; // 等同于[a-zA-Z0-9_],匹配任何字母、数字或下划线
const regex4 = /\W/; // 等同于[^a-zA-Z0-9_],匹配任何非字母、数字或下划线的字符
const regex5 = /\s/; // 匹配任何空白字符(空格、制表符、换行符等)
const regex6 = /\S/; // 匹配任何非空白字符
const regex7 = /./; // 匹配除换行符外的任何单个字符
量词
量词用于指定模式应该匹配的次数:
// 基本量词
const regex1 = /a*/; // 匹配0个或多个a
const regex2 = /a+/; // 匹配1个或多个a
const regex3 = /a?/; // 匹配0个或1个a
const regex4 = /a{3}/; // 精确匹配3个a
const regex5 = /a{2,4}/; // 匹配2到4个a
const regex6 = /a{2,}/; // 匹配至少2个a
// 示例
console.log(/\d+/.test("123")); // true(包含至少一个数字)
console.log(/\d{3}-\d{4}/.test("123-4567")); // true(匹配3个数字-4个数字格式)
默认情况下,量词是"贪婪"的,会尽可能多地匹配字符。添加?
可以使量词变为"非贪婪":
const text = "<div>内容1</div><div>内容2</div>";
// 贪婪匹配(匹配最长的结果)
const greedy = /<div>.*<\/div>/;
console.log(text.match(greedy)[0]); // "<div>内容1</div><div>内容2</div>"
// 非贪婪匹配(匹配最短的结果)
const nonGreedy = /<div>.*?<\/div>/;
console.log(text.match(nonGreedy)[0]); // "<div>内容1</div>"
边界匹配
边界匹配器用于指定模式在文本中的位置:
// 常用边界匹配器
const regex1 = /^start/; // 匹配以"start"开头的字符串
const regex2 = /end$/; // 匹配以"end"结尾的字符串
const regex3 = /\bword\b/; // 匹配单词边界的"word"(前后是非单词字符或字符串的开头/结尾)
const regex4 = /\Bword\B/; // 匹配非单词边界的"word"(前后都是单词字符)
// 示例
console.log(/^Hello/.test("Hello World")); // true
console.log(/^Hello/.test("World Hello")); // false
console.log(/\bcat\b/.test("The cat sat")); // true
console.log(/\bcat\b/.test("category")); // false
分组和捕获
圆括号()
用于分组和捕获匹配的文本:
// 基本分组
const regex1 = /(ab)+/; // 匹配一个或多个"ab"序列
console.log(regex1.test("ababab")); // true
// 捕获组
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const match = "2023-05-15".match(dateRegex);
console.log(match[0]); // "2023-05-15"(完整匹配)
console.log(match[1]); // "2023"(第一个捕获组)
console.log(match[2]); // "05"(第二个捕获组)
console.log(match[3]); // "15"(第三个捕获组)
// 命名捕获组(ES2018)
const namedRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const namedMatch = "2023-05-15".match(namedRegex);
console.log(namedMatch.groups.year); // "2023"
console.log(namedMatch.groups.month); // "05"
console.log(namedMatch.groups.day); // "15"
// 非捕获组
const nonCapture = /(?:ab)+c/; // (?:)表示非捕获组,不会存储匹配结果
const ncMatch = "ababc".match(nonCapture);
console.log(ncMatch[0]); // "ababc"
console.log(ncMatch[1]); // undefined(没有捕获组)
选择和替代
竖线|
用于指定多个可能的模式:
// 选择模式
const regex = /cat|dog|bird/; // 匹配"cat"、"dog"或"bird"
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 fish")); // false
// 与分组结合使用
const fruitRegex = /apple (juice|pie|sauce)/;
console.log(fruitRegex.test("I like apple juice")); // true
console.log(fruitRegex.test("I like apple cake")); // false
前瞻和后顾断言
断言用于指定匹配的上下文条件,但不包含在匹配结果中:
// 正向前瞻(肯定):匹配后面跟着特定模式的位置
const regex1 = /\d+(?=元)/; // 匹配后面跟着"元"的数字
console.log("价格是100元".match(regex1)[0]); // "100"
// 负向前瞻(否定):匹配后面不跟着特定模式的位置
const regex2 = /\d+(?!元)/; // 匹配后面不跟着"元"的数字
console.log("编号A123的商品".match(regex2)[0]); // "123"
// 正向后顾(肯定):匹配前面有特定模式的位置(ES2018)
const regex3 = /(?<=价格是)\d+/; // 匹配前面有"价格是"的数字
console.log("价格是100元".match(regex3)[0]); // "100"
// 负向后顾(否定):匹配前面没有特定模式的位置(ES2018)
const regex4 = /(?<!售价是)\d+元/; // 匹配前面不是"售价是"的"数字+元"
console.log("标价100元".match(regex4)[0]); // "100元"
正则表达式方法
RegExp对象方法
RegExp对象提供了两个主要方法:
// test() - 检查是否存在匹配,返回布尔值
const regex = /\d{3}/;
console.log(regex.test("abc123def")); // true
console.log(regex.test("abcdef")); // false
// exec() - 查找匹配并返回详细信息
const pattern = /(\d{3})-(\d{4})/;
const result = pattern.exec("联系电话:123-4567");
console.log(result[0]); // "123-4567"(完整匹配)
console.log(result[1]); // "123"(第一个捕获组)
console.log(result[2]); // "4567"(第二个捕获组)
console.log(result.index); // 5(匹配开始的位置)
console.log(result.input); // "联系电话:123-4567"(原始字符串)
// 使用g标志时,exec()会记住上次匹配位置
const globalRegex = /\d+/g;
let match;
const text = "有10个苹果和20个橙子";
while ((match = globalRegex.exec(text)) !== null) {
console.log(`找到 ${match[0]} 在位置 ${match.index}`);
}
// 输出:
// 找到 10 在位置 1
// 找到 20 在位置 7
String对象的正则表达式方法
String对象提供了几个使用正则表达式的方法:
const text = "Hello world! Hello JavaScript!";
// match() - 返回匹配结果数组
const matches = text.match(/Hello/g);
console.log(matches); // ["Hello", "Hello"]
// matchAll() - 返回包含所有匹配结果的迭代器(ES2020)
const matchIterator = text.matchAll(/Hello/g);
for (const match of matchIterator) {
console.log(match[0], match.index);
}
// 输出:
// Hello 0
// Hello 13
// search() - 返回第一个匹配的索引,如果没有匹配则返回-1
const index = text.search(/world/);
console.log(index); // 6
// replace() - 替换匹配的文本
const replaced = text.replace(/Hello/g, "Hi");
console.log(replaced); // "Hi world! Hi JavaScript!"
// replaceAll() - 替换所有匹配(ES2021)
const replacedAll = text.replaceAll(/[a-z]+/g, "**");
console.log(replacedAll); // "H** **! H** J**S**!"
// split() - 使用正则表达式分割字符串
const parts = "apple,orange;banana|grape".split(/[,;|]/);
console.log(parts); // ["apple", "orange", "banana", "grape"]
正则表达式高级用法
替换中的特殊模式
在replace()
方法中,可以使用特殊的替换模式:
const text = "姓名:张三,电话:123-4567";
// 使用捕获组引用
const replaced = text.replace(/姓名:(.+),电话:(.+)/, "联系人:$1,联系方式:$2");
console.log(replaced); // "联系人:张三,联系方式:123-4567"
// 使用命名捕获组引用
const namedReplaced = text.replace(/姓名:(?<name>.+),电话:(?<phone>.+)/, "联系人:$<name>,联系方式:$<phone>");
console.log(namedReplaced); // "联系人:张三,联系方式:123-4567"
// 使用函数进行复杂替换
const funcReplaced = text.replace(/(\d+)-(\d+)/, (match, p1, p2) => {
return `${p1}${p2}`;
});
console.log(funcReplaced); // "姓名:张三,电话:1234567"
使用RegExp构造函数动态创建正则表达式
当需要在运行时构建正则表达式时,RegExp构造函数非常有用:
// 从用户输入创建搜索模式
function createSearchRegex(term, caseSensitive = false) {
const flags = caseSensitive ? "g" : "gi";
// 需要转义特殊字符
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(escapedTerm, flags);
}
const userInput = "a+b";
const regex = createSearchRegex(userInput);
console.log(regex); // /a\+b/gi
const text = "计算 a+b 和 A+B";
console.log(text.match(regex)); // ["a+b", "A+B"]
正则表达式的性能考虑
某些正则表达式模式可能导致性能问题:
// 避免灾难性回溯
// 不好的模式:可能导致指数级回溯
const badRegex = /^(a+)+$/;
// 对于输入"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab",会导致极慢的处理
// 更好的替代方案
const betterRegex = /^a+$/;
// 避免不必要的捕获组
// 不好的模式:不必要的捕获
const inefficientRegex = /(a)|(b)|(c)/;
// 更好的替代方案:使用非捕获组
const efficientRegex = /(?:a)|(?:b)|(?:c)/;
// 或者更简单的
const simpleRegex = /[abc]/;
// 使用适当的量词和锚点来限制匹配范围
const limitedRegex = /^[a-z]{1,10}$/; // 只匹配1到10个小写字母
常用正则表达式示例
验证模式
// 电子邮件验证(基本版)
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
console.log(emailRegex.test("user@example.com")); // true
console.log(emailRegex.test("invalid-email")); // false
// 中国手机号验证
const phoneRegex = /^1[3-9]\d{9}$/;
console.log(phoneRegex.test("13812345678")); // true
console.log(phoneRegex.test("12345678901")); // false
// 中国身份证号(18位)
const idCardRegex = /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;
console.log(idCardRegex.test("11010119900307123X")); // true
// URL验证(基本版)
const urlRegex = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/[a-zA-Z0-9-._~:/?#[\]@!$&'()*+,;=]*)?$/;
console.log(urlRegex.test("https://www.example.com/path")); // true
// 强密码验证(至少8位,包含大小写字母、数字和特殊字符)
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,}$/;
console.log(passwordRegex.test("Passw0rd!")); // true
console.log(passwordRegex.test("password")); // false
提取和替换模式
// 提取所有HTML标签
const html = "<div><p>内容</p><a href='#'>链接</a></div>";
const tags = html.match(/<[^>]+>/g);
console.log(tags); // ["<div>", "<p>", "</p>", "<a href='#'>", "</a>", "</div>"]
// 提取URL中的查询参数
const url = "https://example.com/search?name=张三&age=25&city=北京";
const queryParams = {};
url.replace(/[?&]([^=&]+)=([^&]*)/g, (match, key, value) => {
queryParams[key] = decodeURIComponent(value);
return match;
});
console.log(queryParams); // { name: "张三", age: "25", city: "北京" }
// 格式化日期
const dateStr = "2023-05-15";
const formattedDate = dateStr.replace(/(\d{4})-(\d{2})-(\d{2})/, "$1年$2月$3日");
console.log(formattedDate); // "2023年05月15日"
// 将驼峰命名转换为短横线命名
const camelCase = "backgroundColor";
const kebabCase = camelCase.replace(/([A-Z])/g, "-$1").toLowerCase();
console.log(kebabCase); // "background-color"
// 将数字格式化为千分位
const number = "1234567.89";
const formatted = number.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
console.log(formatted); // "1,234,567.89"
字符串处理
// 去除字符串两端的空白字符
const trimRegex = /^\s+|\s+$/g;
const trimmed = " Hello World ".replace(trimRegex, "");
console.log(trimmed); // "Hello World"
// 将多个连续空白字符替换为一个空格
const normalizeSpace = "Hello World !".replace(/\s+/g, " ");
console.log(normalizeSpace); // "Hello World !"
// 检查是否包含中文字符
const containsChinese = /[\u4e00-\u9fa5]/.test("Hello世界");
console.log(containsChinese); // true
// 提取所有数字
const numbers = "价格:100元,数量:5个".match(/\d+/g);
console.log(numbers); // ["100", "5"]
// 检查字符串是否为有效的JSON格式
function isValidJSON(str) {
try {
JSON.parse(str);
return true;
} catch (e) {
return false;
}
}
console.log(isValidJSON('{"name":"张三"}')); // true
console.log(isValidJSON('{name:"张三"}')); // false
正则表达式调试技巧
测试和调试工具
有多种工具可以帮助测试和调试正则表达式:
- 在线工具:如Regex101、RegExr等
- 浏览器控制台
- 专用的正则表达式可视化工具
分解复杂正则表达式
将复杂的正则表达式分解为更小的部分,逐步构建和测试:
// 构建复杂的电子邮件验证正则表达式
// 1. 用户名部分
const usernameRegex = /^[a-zA-Z0-9._-]+/;
// 2. 域名部分
const domainRegex = /@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
// 3. 完整的电子邮件正则表达式
const emailRegex = new RegExp(
usernameRegex.source + domainRegex.source
);
console.log(emailRegex); // /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/
注释正则表达式
使用x
(扩展)标志和注释可以使复杂的正则表达式更易读(注意:JavaScript原生不支持x
标志,但可以通过其他方式实现类似效果):
// 使用模板字符串和构造函数创建可读的正则表达式
const phoneRegexPattern = `
^ # 开始
1 # 第一位是1
[3-9] # 第二位是3-9
\\d{9} # 后面9位数字
$ # 结束
`;
// 移除空白和注释
const cleanPattern = phoneRegexPattern
.replace(/\s+/g, '')
.replace(/#.*$/gm, '');
const phoneRegex = new RegExp(cleanPattern);
console.log(phoneRegex); // /^1[3-9]\d{9}$/
总结
正则表达式是处理文本的强大工具,在JavaScript中有广泛的应用。本文介绍了正则表达式的基础语法、创建方法、常用模式和实际应用示例。
关键要点:
- 正则表达式可以通过字面量
/.../
或RegExp
构造函数创建 - 标志(如
g
、i
、m
)修改正则表达式的匹配行为 - 特殊字符、字符类和量词用于构建复杂的匹配模式
- 分组和捕获允许提取匹配的特定部分
- JavaScript提供了多种使用正则表达式的方法,如
test()
、match()
和replace()
- 复杂的正则表达式应该分解、测试和注释,以提高可维护性
通过掌握正则表达式,你可以更有效地处理字符串验证、提取和替换等任务,提高代码的效率和灵活性。