各位靓仔靓女们,今天咱们来聊聊 JavaScript 正则表达式这玩意儿,特别是它那“贪得无厌”和“谦让有礼”的匹配方式,还有那神秘莫测的“前后瞻顾”断言。准备好了吗?系好安全带,开始咱们的正则表达式之旅!
开场白:正则表达式,你这磨人的小妖精!
正则表达式,简称 RegExp,是用来匹配文本模式的强大工具。在 JavaScript 的世界里,它就像一把瑞士军刀,可以用来搜索、替换、验证各种各样的字符串。但是,这把刀用得不好,也会割到手的。今天,咱们就来学习如何更好地驾驭这把刀。
第一章:贪婪模式 (Greedy Matching) – 贪吃蛇的本性
默认情况下,JavaScript 的 RegExp 引擎是“贪婪”的。这意味着它会尽可能多地匹配字符,直到整个字符串的末尾,然后才开始回溯,寻找可能的匹配项。
举个栗子:
const str = "<h1>Hello</h1><h2>World</h2>";
const greedyRegex = /<.*>/; // 贪婪模式
const match = str.match(greedyRegex);
console.log(match); // 输出: ["<h1>Hello</h1><h2>World</h2>", index: 0, input: "<h1>Hello</h1><h2>World</h2>", groups: undefined]
在这个例子中,/<.*>/
表达式试图匹配所有以 <
开头,以 >
结尾的内容。由于贪婪模式的本性,它一口气吞掉了整个字符串,从第一个 <
到最后一个 >
,形成了一个超大的匹配项。
贪婪模式的特点:
- 尽可能多地匹配。
- 回溯以寻找可能的匹配项。
- 是 RegExp 引擎的默认行为。
第二章:非贪婪模式 (Non-Greedy Matching) – 谦让的绅士
有时候,我们并不希望 RegExp 引擎这么贪婪,而是希望它尽可能少地匹配。这时候,就要请出我们的“非贪婪模式”了。要开启非贪婪模式,只需在量词(如 *
, +
, ?
, {}
)后面加上一个问号 ?
。
还是上面的例子,咱们换成非贪婪模式:
const str = "<h1>Hello</h1><h2>World</h2>";
const nonGreedyRegex = /<.*?>/; // 非贪婪模式
const match = str.match(nonGreedyRegex);
console.log(match); // 输出: ["<h1>", index: 0, input: "<h1>Hello</h1><h2>World</h2>", groups: undefined]
这次,/<.*?>/
表达式只匹配了第一个 <h1>
,因为它在遇到第一个 >
的时候就停止了匹配,不再继续吞噬后面的字符。
非贪婪模式的特点:
- 尽可能少地匹配。
- 在量词后添加
?
开启。 - 也叫做“懒惰模式” (Lazy Matching)。
贪婪与非贪婪的对比:
特性 | 贪婪模式 (Greedy) | 非贪婪模式 (Non-Greedy) |
---|---|---|
匹配方式 | 尽可能多 | 尽可能少 |
符号 | 无 | ? |
适用场景 | 需要匹配大块内容时 | 需要精确匹配时 |
第三章:前后瞻顾 (Lookaround) – 侦察兵的视角
前后瞻顾断言允许我们在匹配一个模式的同时,检查该模式的前面或后面的字符是否满足某些条件,但又不将这些字符包含在最终的匹配结果中。这就像一个侦察兵,先观察敌情,再决定是否行动。
前后瞻顾断言分为四种:
-
正向肯定预查 (Positive Lookahead):
(?=pattern)
- 匹配后面跟着
pattern
的内容,但不包含pattern
。
- 匹配后面跟着
-
正向否定预查 (Negative Lookahead):
(?!pattern)
- 匹配后面不跟着
pattern
的内容。
- 匹配后面不跟着
-
反向肯定预查 (Positive Lookbehind):
(?<=pattern)
- 匹配前面是
pattern
的内容,但不包含pattern
。(注意:并非所有 JavaScript 引擎都支持反向预查,特别是老版本的浏览器。)
- 匹配前面是
-
反向否定预查 (Negative Lookbehind):
(?<!pattern)
- 匹配前面不是
pattern
的内容。(同样,并非所有 JavaScript 引擎都支持反向预查。)
- 匹配前面不是
3.1 正向肯定预查 (Positive Lookahead) – 瞄准目标
假设我们想匹配所有后面跟着 USD
的数字,但不包含 USD
本身:
const str = "Price: 100USD, Discount: 20USD";
const regex = /d+(?=USD)/g; // 正向肯定预查
const match = str.match(regex);
console.log(match); // 输出: ["100", "20"]
这里,/d+(?=USD)/g
表达式的意思是:匹配一个或多个数字 (d+
),但只有当这些数字后面紧跟着 USD
时才匹配。(?=USD)
就是正向肯定预查,它只检查后面的字符,但不包含在最终的匹配结果中。
3.2 正向否定预查 (Negative Lookahead) – 排除异己
现在,我们想匹配所有后面不跟着 USD
的数字:
const str = "Price: 100USD, Quantity: 50, Tax: 10EUR";
const regex = /d+(?!USD)/g; // 正向否定预查
const match = str.match(regex);
console.log(match); // 输出: ["50", "10"]
/d+(?!USD)/g
表达式的意思是:匹配一个或多个数字 (d+
),但只有当这些数字后面没有紧跟着 USD
时才匹配。(?!USD)
就是正向否定预查,它确保匹配的数字后面不是 USD
。
3.3 反向肯定预查 (Positive Lookbehind) – 确认身份
假设我们想匹配所有前面是 $
符号的数字,但不包含 $
符号(注意:部分浏览器可能不支持):
const str = "Price: $100, Discount: $20";
const regex = /(?<=$)(d+)/g; // 反向肯定预查
const match = str.match(regex);
console.log(match); // 输出: ["100", "20"]
/(?<=$)(d+)/g
表达式的意思是:匹配一个或多个数字 (d+
),但只有当这些数字前面紧跟着 $
符号时才匹配。(?<=$)
就是反向肯定预查,它只检查前面的字符,但不包含在最终的匹配结果中。
3.4 反向否定预查 (Negative Lookbehind) – 排除嫌疑
现在,我们想匹配所有前面不是 $
符号的数字(同样,部分浏览器可能不支持):
const str = "Price: $100, Quantity: 50, Tax: €10";
const regex = /(?<!$)d+/g; // 反向否定预查
const match = str.match(regex);
console.log(match); // 输出: ["50", "10"] (取决于浏览器支持情况)
/(?<!$)d+/g
表达式的意思是:匹配一个或多个数字 (d+
),但只有当这些数字前面没有紧跟着 $
符号时才匹配。(?<!$)
就是反向否定预查,它确保匹配的数字前面不是 $
符号。
前后瞻顾断言的对比:
断言类型 | 符号 | 作用 |
---|---|---|
正向肯定预查 (Lookahead) | (?=pattern) |
匹配后面跟着 pattern 的内容,但不包含 pattern 。 |
正向否定预查 (Lookahead) | (?!pattern) |
匹配后面不跟着 pattern 的内容。 |
反向肯定预查 (Lookbehind) | (?<=pattern) |
匹配前面是 pattern 的内容,但不包含 pattern 。(部分引擎不支持) |
反向否定预查 (Lookbehind) | (?<!pattern) |
匹配前面不是 pattern 的内容。(部分引擎不支持) |
第四章:实战演练 – 正则表达式的妙用
说了这么多理论,咱们来几个实际的例子,看看如何运用这些知识解决实际问题。
4.1 验证密码强度:
假设我们需要验证一个密码是否符合以下条件:
- 至少包含一个大写字母
- 至少包含一个小写字母
- 至少包含一个数字
- 长度至少为 8 位
我们可以使用前后瞻顾断言来实现:
function validatePassword(password) {
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*d).{8,}$/;
return regex.test(password);
}
console.log(validatePassword("P@sswOrd123")); // 输出: true
console.log(validatePassword("password")); // 输出: false
console.log(validatePassword("Password1")); // 输出: false
解释:
^
: 匹配字符串的开头。(?=.*[a-z])
: 正向肯定预查,确保字符串中至少包含一个小写字母。(?=.*[A-Z])
: 正向肯定预查,确保字符串中至少包含一个大写字母。(?=.*d)
: 正向肯定预查,确保字符串中至少包含一个数字。.{8,}
: 匹配任意字符,至少 8 位。$
: 匹配字符串的结尾。
4.2 提取 HTML 标签的内容:
假设我们想从一段 HTML 代码中提取所有 <a>
标签的 href
属性值:
const html = '<a href="https://www.google.com">Google</a><a href="https://www.baidu.com">Baidu</a>';
const regex = /<a href="(?<url>.*?)">/g;
let match;
let urls = [];
while ((match = regex.exec(html)) !== null) {
urls.push(match.groups.url);
}
console.log(urls); // 输出: ["https://www.google.com", "https://www.baidu.com"]
解释:
<a href="
: 匹配<a>
标签的开头。(?<url>.*?)
: 使用命名捕获组url
匹配href
属性值,使用非贪婪模式,尽可能少地匹配。">
: 匹配<a>
标签的结尾。regex.exec(html)
: 循环匹配 HTML 代码,并将匹配结果存储在match
变量中。match.groups.url
: 从match
对象中提取命名捕获组url
的值。
4.3 替换字符串中的特定单词:
假设我们想将字符串中所有出现的 "apple" 替换为 "orange",但只替换独立的单词 "apple",而不是 "pineapple" 中的 "apple":
const str = "I like apple and pineapple.";
const regex = /bappleb/g; // b 表示单词边界
const newStr = str.replace(regex, "orange");
console.log(newStr); // 输出: "I like orange and pineapple."
解释:
b
: 匹配单词边界,确保匹配的是独立的单词 "apple"。apple
: 匹配单词 "apple"。b
: 匹配单词边界。str.replace(regex, "orange")
: 将匹配到的 "apple" 替换为 "orange"。
第五章:注意事项 – 正则表达式的坑
正则表达式很强大,但也容易出错。以下是一些需要注意的地方:
- 性能问题: 复杂的正则表达式可能会导致性能问题,特别是当处理大量文本时。尽量简化正则表达式,避免过度使用回溯。
- 可读性问题: 复杂的正则表达式难以阅读和维护。添加注释,将正则表达式分解成更小的部分,可以提高可读性。
- 兼容性问题: 不同的 JavaScript 引擎对正则表达式的支持程度可能有所不同。特别是反向预查,在老版本的浏览器中可能无法使用。
- 转义问题: 正则表达式中的一些字符具有特殊含义,需要进行转义。例如,要匹配
.
字符,需要使用.
。
总结:正则表达式的修炼之路
正则表达式是一门需要不断学习和实践的技能。掌握贪婪与非贪婪匹配、前后瞻顾断言,以及各种正则表达式的语法和技巧,可以让你在文本处理方面事半功倍。
记住,正则表达式就像一把锋利的宝剑,用得好可以披荆斩棘,用得不好则会伤到自己。希望今天的讲座能帮助你更好地驾驭这把宝剑,在编程的道路上越走越远!
最后,留几个思考题给大家:
- 如何使用正则表达式提取一个字符串中的所有邮箱地址?
- 如何使用正则表达式验证一个字符串是否是有效的 IP 地址?
- 如何使用正则表达式将一个字符串中的所有 HTML 标签去除?
欢迎大家在评论区分享你的答案和心得体会!咱们下期再见!