本文内容为笔者在 freecodecamp 学习的笔记,里面的内容均来自 freecodecamp,特此说明。
推荐阅读文章:https://juejin.cn/post/6844903845227659271
在编程语言中,正则表达式用于匹配指定的字符串。通过正则表达式创建匹配模式(规则)可以帮你完成指定匹配。
如果你想要在字符串"The dog chased the cat"
中匹配到"the"
这个单词,你可以使用如下正则表达式:/the/
。
JavaScript 中有多种使用正则表达式的方法。测试正则表达式的一种方法是使用.test()
方法。.test()
方法会把你编写的正则表达式应用到一个字符串(即括号内的内容),如果你的匹配模式成功匹配到字符,则返回true
,反之,返回false
。
let testStr = "freeCodeCamp";
let testRegex = /Code/;
testRegex.test(testStr);
// Returns true
使用.test()
方法,检测字符串myString
是否符合正则表达式myRegex
定义的规则。
let testStr = "Hello, my name is Kevin.";
let testRegex = /Kevin/;
testRegex.test(testStr);
// Returns true
let wrongRegex = /kevin/;
wrongRegex.test(testStr);
// Returns false
任何其他形式的"Kevin"
都不会被匹配。例如,正则表达式/Kevin/
不会匹配"kevin"
或者"KEVIN"
。
使用|
操作符来匹配多个规则。此操作符匹配在它之前或之后的匹配模式。例如,如果你想匹配"yes"
或"no"
,你需要的正则表达式是/yes|no/
。
let petString = "James has a pet cat.";
let petRegex = /dog|cat|bird|fish/;
let result = petRegex.test(petString);
大小写(或者字母大小写)是大写字母和小写字母的区别。大写字母的例子有"A"
、"B"
和"C"
。小写字母的例子有"a"
、"b"
和"c"
。忽略大小写的标志——i
。示例/ignorecase/i
。这个字符串可以匹配字符串"ignorecase"
、"igNoreCase"
和"IgnoreCase"
。
let myString = "freeCodeCamp";
let fccRegex = /freeCodeCamp/i; // 修改这一行
let result = fccRegex.test(myString);
使用.match()
方法来提取你找到的实际匹配项。
let extractStr = "Extract the word 'coding' from this string.";
let codingRegex = /coding/;
let result = extractStr.match(codingRegex);
若要多次搜寻或提取匹配模式,你可以使用g
标志。
let twinkleStar = "Twinkle, twinkle, little star";
let starRegex = /Twinkle/gi;
let result = twinkleStar.match(starRegex);
有时你不会(或不需要)知道匹配模式中的确切字符。如果要精确匹配到完整的单词,那出现一个拼写错误就会匹配不到。幸运的是,你可以使用通配符.
来处理这种情况。
通配符.
将匹配任何一个字符。通配符也叫dot
或period
。你可以像使用正则表达式中任何其他字符一样使用通配符。例如,如果你想匹配"hug"
、"huh"
、"hut"
和"hum"
,你可以使用正则表达式/hu./
匹配以上四个单词。
let humStr = "I'll hum a song";
let hugStr = "Bear hug";
let huRegex = /hu./;
humStr.match(huRegex); // Returns ["hum"]
hugStr.match(huRegex); // Returns ["hug"]
使用字符集
搜寻具有一定灵活性的文字匹配模式。字符集允许你通过把它们放在方括号([
和]
)之间的方式来定义一组你需要匹配的字符串。
例如,你想要匹配"bag"
、"big"
和"bug"
,但是不想匹配"bog"
。你可以创建正则表达式/b[aiu]g/
来执行此操作。[aiu]
是只匹配字符"a"
、"i"
或者"u"
的字符集。
使用元音字符集(
a
、e
、i
、o
、u
)在你的正则表达式vowelRegex
中匹配到字符串quoteSample
中的所有元音。
let quoteSample = "Beware of bugs in the above code; I have only proved it correct, not tried it.";
let vowelRegex = /[aeiou]/gi;
let result = quoteSample.match(vowelRegex);
当需要匹配大量字符(例如,字母表中的每个字母)时,使用连字符
(-
)来定义要匹配的字符范围。例如,要匹配小写字母a
到e
,你可以使用[a-e]
。
匹配字符串
quoteSample
中的所有字母。
let quoteSample = "The quick brown fox jumps over the lazy dog.";
let alphabetRegex = /[a-z]/gi;
let result = quoteSample.match(alphabetRegex);
使用连字符(-
)匹配字符范围并不仅限于字母。它还可以匹配一系列数字。例如,/[0-5]/
匹配0
和5
之间的任意数字,包含0
和5
。此外,还可以在单个字符集中组合一系列字母和数字。
创建一个正则表达式,使其可以匹配
h
和s
之间的一系列字母,以及2
和6
之间的一系列数字。
let quoteSample = "Blueberry 3.141592653s are delicious.";
let myRegex = /[h-s2-6]/gi;
let result = quoteSample.match(myRegex);
要创建否定字符集
,你需要在开始括号后面和不想匹配的字符前面放置插入字符
(即^
)。例如,/[^aeiou]/gi
匹配所有非元音字符。注意,字符.
、!
、[
、@
、/
和空白字符等也会被匹配,该否定字符集仅排除元音字符。
创建一个匹配所有非数字或元音字符的正则表达式。请记得在正则表达式中包含恰当的标志。
let quoteSample = "3 blind mice.";
let myRegex = /[^aeiou^0-9]/gi;
let result = quoteSample.match(myRegex);
要匹配出现一次或者连续多次的的字符(或字符组)。这意味着它至少出现一次,并且可能重复出现。可以使用+
符号来检查情况是否如此。记住,字符或匹配模式必须一个接一个地连续出现。
例如,/a+/g
会在"abc"
中匹配到一个匹配项,并且返回["a"]
。因为+
的存在,它也会在"aabc"
中匹配到一个匹配项,然后返回["aa"]
。如果它是检查字符串"abab"
,它将匹配到两个匹配项并且返回["a", "a"]
,因为a
字符不连续,在它们之间有一个b
字符。最后,因为在字符串"bcd"
中没有"a"
,因此找不到匹配项。
在字符串
"Mississippi"
中匹配到出现一次或多次的字母s
的匹配项。编写一个使用+
符号的正则表达式。
let difficultSpelling = "Mississippi";
let myRegex = /s+/g;
let result = difficultSpelling.match(myRegex);
*
,可以匹配出现零次或多次的字符。
使用
*
符号在chewieQuote
中匹配"A"
及其之后出现的零个或多个"a"
。你的正则表达式不需要使用修饰符,也不需要匹配引号。
let chewieQuote = "Aaaaaaaaaaaaaaaarrrgh!";
let chewieRegex = /Aa*/g;
let result = chewieQuote.match(chewieRegex);
在正则表达式中,贪婪
匹配会匹配到符合正则表达式匹配模式的字符串的最长可能部分,并将其作为匹配项返回。另一种方案称为懒惰
匹配,它会匹配到满足正则表达式的字符串的最小可能部分。
你可以将正则表达式/t[a-z]*i/
应用于字符串"titanic"
。这个正则表达式是一个以t
开始,以i
结束,并且中间有一些字母的匹配模式。
正则表达式默认是贪婪
匹配,因此匹配返回为["titani"]
。它会匹配到适合该匹配模式的最大子字符串。
但是,你可以使用?
字符来将其变成懒惰
匹配。调整后的正则表达式/t[a-z]*?i/
匹配字符串"titanic"
返回["ti"]
。
修复正则表达式
/<.*>/
,让它返回 HTML 标签,而不是文本
"
。请记得在正则表达式中使用通配符Winter is coming
".
来匹配任意字符。
let text = "Winter is coming
";
let myRegex = /<.*?>/;
let result = text.match(myRegex);
编写一个
贪婪
正则表达式,在一组其他人中匹配到一个或多个罪犯。罪犯由大写字母C
表示。
let crowd = 'P1P2P3P4P5P6CCCP7P8P9';
let reCriminals = /C+/;
let matchedCriminals = crowd.match(reCriminals);
console.log(matchedCriminals);
使用字符集
中的插入
符号(^
)来创建一个否定字符集
,形如[^thingsThatWillNotBeMatched]
。在字符集
之外,插入
符号用于字符串的开头搜寻匹配模式。
let firstString = "Ricky is first and can be found.";
let firstRegex = /^Ricky/;
firstRegex.test(firstString);
// Returns true
let notFirst = "You can't find Ricky now.";
firstRegex.test(notFirst);
// Returns false
使用正则表达式的美元
符号$
来搜寻字符串的结尾。
let theEnding = "This is a never ending story";
let storyRegex = /story$/;
storyRegex.test(theEnding);
// Returns true
let noEnding = "Sometimes a story will have to end";
storyRegex.test(noEnding);
// Returns false
使用字符类,你可以使用[a-z]
搜寻字母表中的所有字母。JavaScript 中与字母表匹配的最接近的字符类是\w
,这个缩写等同于[A-Za-z0-9_]
。它不仅可以匹配大小写字母和数字,注意,它还会匹配下划线字符(_
)。
let longHand = /[A-Za-z0-9_]+/;
let shortHand = /\w+/;
let numbers = "42";
let varNames = "important_var";
longHand.test(numbers); // Returns true
shortHand.test(numbers); // Returns true
longHand.test(varNames); // Returns true
shortHand.test(varNames); // Returns true
可以使用\W
搜寻和\w
相反的匹配模式。注意,相反匹配模式使用大写字母。此缩写与[^A-Za-z0-9_]
是一样的。
使用缩写
\W
来计算不同引号和字符串中非字母数字字符的数量。
let quoteSample = "The five boxing wizards jump quickly.";
let nonAlphabetRegex = /\W/g;
let result = quoteSample.match(nonAlphabetRegex).length;
查找数字字符的缩写是\d
,注意是小写的d
。这等同于字符类[0-9]
,它查找 0 到 9 之间任意数字的单个字符。
使用缩写
\d
来计算电影标题中有多少个数字。书面数字(“six” 而不是 6)不计算在内。
let numString = "Your sandwich will be $5.00";
let numRegex = /\d/g;
let result = numString.match(numRegex).length;
查找非数字字符的缩写是\D
。这等同于字符串[^0-9]
,它查找不是 0 - 9 之间数字的单个字符。
使用非数字缩写
\D
来计算电影标题中有多少非数字。
let numString = "Your sandwich will be $5.00";
let noNumRegex = /\D/g;
let result = numString.match(noNumRegex).length;
用户在创建用户名时必须遵守的一些简单规则。
用户名中的数字必须在最后,且数字可以有零个或多个。
用户名字母可以是小写字母和大写字母。
用户名长度必须至少为两个字符。两位用户名只能使用字母。
let username = "JackOfAllTrades";
let userCheck = /^[a-z][a-z]+\d*$|^[a-z]\d\d+$/i;
let result = userCheck.test(username);
^ 输入的开始
[a-z] 第一个字符是一个字母
[a-z]+ 后面的字符是字母
\d*$ 输入以0或多个数字结束
| 或者
1 第一个字符是一个字母
\d\d+ 以下字符是2个或以上的数字
$ 输入结束
使用\s
搜寻空格,其中s
是小写。此匹配模式不仅匹配空格,还匹配回车符、制表符、换页符和换行符,你可以将其视为与[\r\t\f\n\v]
类似。
修改正则表达式
countWhiteSpace
查找字符串中的多个空白字符。
let sample = "Whitespace is important in separating words";
let countWhiteSpace = /\s/g;
let result = sample.match(countWhiteSpace);
使用\S
搜寻非空白字符,其中S
是大写。此匹配模式将不匹配空格、回车符、制表符、换页符和换行符。你可以认为这类似于字符类[^\r\t\f\n\v]
。
修改正则表达式
countNonWhiteSpace
以查找字符串中的多个非空字符。
let sample = "Whitespace is important in separating words";
let countNonWhiteSpace = /\S/g;
let result = sample.match(countNonWhiteSpace);
使用数量说明符
指定匹配模式的上下限。数量说明符与花括号({
和}
)一起使用。你可以在花括号之间放两个数字,这两个数字代表匹配模式的上限和下限。
例如,要在字符串"ah"
中匹配仅出现3
到5
次的字母a
,你的正则表达式应为/a{3,5}h/
。
修改正则表达式
ohRegex
以匹配在"Oh no"
中仅出现3
到6
次的字母h
。
let ohStr = "Ohhh no";
let ohRegex = /Oh{3,6}\sno/;
let result = ohRegex.test(ohStr);
只想指定匹配模式的下限而不需要指定上限。为此,在第一个数字后面跟一个逗号即可。例如,要匹配至少出现3
次字母a
的字符串"hah"
,你的正则表达式应该是/ha{3,}h/
。
修改正则表达式
haRegex
,匹配包含四个或更多字母z
的单词"Hazzah"
。
let haStr = "Hazzzzah";
let haRegex = /Haz{4,}ah/;
let result = haRegex.test(haStr);
要指定一定数量的匹配模式,只需在大括号之间放置一个数字。例如,要只匹配字母a
出现3
次的单词"hah"
,你的正则表达式应为/ha{3}h/
。
修改正则表达式
timRegex
,以匹配仅有四个字母单词m
的单词"Timber"
。
let timStr = "Timmmmber";
let timRegex = /Tim{4}ber/;
let result = timRegex.test(timStr);
使用问号?
指定可能存在的元素。这将检查前面的零个或一个元素。你可以将此符号视为前面的元素是可选的。
修改正则表达式
favRegex
以匹配美式英语(favorite)和英式英语(favourite)的单词版本。
let favWord = "favorite";
let favRegex = /favou?rite/;
let result = favRegex.test(favWord);
先行断言
是告诉 JavaScript 在字符串中向前查找的匹配模式。当你想要在同一个字符串上搜寻多个匹配模式时,这可能非常有用。
有两种先行断言
:正向先行断言
和负向先行断言
。
正向先行断言
会查看并确保搜索匹配模式中的元素存在,但实际上并不匹配。正向先行断言的用法是(?=...)
,其中...
就是需要存在但不会被匹配的部分。
另一方面,负向先行断言
会查看并确保搜索匹配模式中的元素不存在。负向先行断言的用法是(?!...)
,其中...
是你希望不存在的匹配模式。如果负向先行断言部分不存在,将返回匹配模式的其余部分。
let quit = "qu";
let noquit = "qt";
let quRegex= /q(?=u)/;
let qRegex = /q(?!u)/;
quit.match(quRegex); // Returns ["q"]
noquit.match(qRegex); // Returns ["q"]
先行断言
的更实际用途是检查一个字符串中的两个或更多匹配模式。这里有一个简单的密码检查器,密码规则是 3 到 6 个字符且至少包含一个数字:
let password = "abc123";
let checkPass = /(?=\w{3,6})(?=\D*\d)/;
checkPass.test(password); // Returns true
在正则表达式
pwRegex
中使用先行断言
以匹配至少5个字符且有两个连续数字的密码。
let sampleWord = "astronaut";
let pwRegex = /(?=\w{6})(?=\w*\d{2})/;
let result = pwRegex.test(sampleWord);
一些你所搜寻的匹配模式会在字符串中出现多次,手动重复该正则表达式太浪费了。有一种更好的方法可以指定何时在字符串中会有多个重复的子字符串。
你可以使用捕获组
搜寻重复的子字符串。括号(
和)
可以用来匹配重复的子字符串。你只需要把重复匹配模式的正则表达式放在括号中即可。
要指定重复字符串将出现的位置,可以使用反斜杠(\
)后接一个数字。这个数字从 1 开始,随着你使用的每个捕获组的增加而增加。这里有一个示例,\1
可以匹配第一个组。
下面的示例匹配任意两个被空格分割的单词:
let repeatStr = "regex regex";
let repeatRegex = /(\w+)\s\1/;
repeatRegex.test(repeatStr); // Returns true
repeatStr.match(repeatRegex); // Returns ["regex regex", "regex"]
在字符串上使用.match()
方法将返回一个数组,其中包含它匹配的字符串及其捕获组。
在正则表达式
reRegex
中使用捕获组
,以匹配在字符串中仅重复三次的数字,每一个都由空格分隔。
let repeatNum = "42 42 42";
let reRegex = /^(\d+)\s\1\s\1$/;
let result = reRegex.test(repeatNum);
可以使用字符串上.replace()
方法来搜索并替换字符串中的文本。.replace()
的输入首先是你想要搜索的正则表达式匹配模式,第二个参数是用于替换匹配的字符串或用于执行某些操作的函数。
编写一个正则表达式,以搜索字符串
"good"
。然后更新变量replaceText
,用字符串"okey-dokey"
替换"good"
。
let huhText = "This sandwich is good.";
let fixRegex = /good/;
let replaceText = "okey-dokey";
let result = huhText.replace(fixRegex, replaceText);
let hello = " Hello, World! ";
let wsRegex = /^\s+|\s+$/g;
let result = hello.replace(wsRegex, "");
小练习
今天笔者偶然看到一篇文章,JavaScript trim函数大赏
去除字符串空格的方法中,大都用到了正则。各位可以试着看看,结合上面的知识点,更加深刻的理解正则的妙用
String.prototype.trim = function () {
return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}
看起来不怎么样,动用了两次正则替换,实际速度非常惊人,主要得益于浏览器的内部优化。一个著名的例子字符串拼接,直接相加比用Array做成的StringBuffer还快。base2类库使用这种实现。
String.prototype.trim = function () {
return this.replace(/^\s+/, '').replace(/\s+$/, '');
}
和实现1很相似,但稍慢一点,主要原因是它最先是假设至少存在一个空白符。Prototype.js使用这种实现,不过其名字为strip,因为Prototype的方法都是力求与Ruby同名。
String.prototype.trim = function () {
return this.substring(Math.max(this.search(/\S/), 0), this.search(/\S\s*$/) + 1);
}
以截取方式取得空白部分(当然允许中间存在空白符),总共调用了四个原生方法。设计得非常巧妙,substring以两个数字作为参数。Math.max以两个数字作参数,search则返回一个数字。速度比上面两个慢一点,但比下面大多数都快。
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, '');
}
这个可以称得上实现2的简化版,就是利用候选操作符连接两个正则。但这样做就失去了浏览器优化的机会,比不上实现3。由于看来很优雅,许多类库都使用它,如JQuery与mootools
String.prototype.trim = function () {
var str = this;
str = str.match(/\S+(?:\s+\S+)*/);
return str ? str[0] : '';
}
match是返回一个数组,因此原字符串符合要求的部分就成为它的元素。为了防止字符串中间的空白符被排除,我们需要动用到非捕获性分组(?:exp)。由于数组可能为空,我们在后面还要做进一步的判定。好像浏览器在处理分组上比较无力,一个字慢。所以不要迷信正则,虽然它基本上是万能的。
String.prototype.trim = function () {
return this.replace(/^\s*(\S*(\s+\S+)*)\s*$/, '$1');
}
把符合要求的部分提供出来,放到一个空字符串中。不过效率很差,尤其是在IE6中。
String.prototype.trim = function () {
return this.replace(/^\s*(\S*(?:\s+\S+)*)\s*$/, '$1');
}
和实现6很相似,但用了非捕获分组进行了优点,性能效之有一点点提升。
String.prototype.trim = function () {
return this.replace(/^\s*((?:[\S\s]*\S)?)\s*$/, '$1');
}
沿着上面两个的思路进行改进,动用了非捕获分组与字符集合,用?顶替了*,效果非常惊人。尤其在IE6中,可以用疯狂来形容这次性能的提升,直接秒杀火狐。
String.prototype.trim = function () {
return this.replace(/^\s*([\S\s]*?)\s*$/, '$1');
}
这次是用懒惰匹配顶替非捕获分组,在火狐中得到改善,IE没有上次那么疯狂。
String.prototype.trim = function () {
var str = this,
whitespace = ' \n\r\t\f\x0b\xa0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000';
for (var i = 0, len = str.length; i < len; i++) {
if (whitespace.indexOf(str.charAt(i)) === -1) {
str = str.substring(i);
break;
}
}
for (i = str.length - 1; i >= 0; i--) {
if (whitespace.indexOf(str.charAt(i)) === -1) {
str = str.substring(0, i + 1);
break;
}
}
return whitespace.indexOf(str.charAt(0)) === -1 ? str : '';
}
我只想说,搞出这个的人已经不是用牛来形容,已是神一样的级别。它先是把可能的空白符全部列出来,在第一次遍历中砍掉前面的空白,第二次砍掉后面的空白。全过程只用了indexOf与substring这个专门为处理字符串而生的原生方法,没有使用到正则。速度快得惊人,估计直逼上内部的二进制实现,并且在IE与火狐(其他浏览器当然也毫无疑问)都有良好的表现。速度都是零毫秒级别的。
String.prototype.trim = function () {
var str = this,
str = str.replace(/^\s+/, '');
for (var i = str.length - 1; i >= 0; i--) {
if (/\S/.test(str.charAt(i))) {
str = str.substring(0, i + 1);
break;
}
}
return str;
}
实现10已经告诉我们普通的原生字符串截取方法是远胜于正则替换,虽然是复杂一点。但只要正则不过于复杂,我们就可以利用浏览器对正则的优化,改善程序执行效率,如实现8在IE的表现。我想通常不会有人在项目中应用实现10,因为那个whitespace 实现太长太难记了(当然如果你在打造一个类库,它绝对是首先)。实现11可谓其改进版,前面部分的空白由正则替换负责砍掉,后面用原生方法处理,效果不逊于原版,但速度都是非常逆天。
String.prototype.trim = function () {
var str = this,
str = str.replace(/^\s\s*/, ''),
ws = /\s/,
i = str.length;
while (ws.test(str.charAt(--i)));
return str.slice(0, i + 1);
}
实现10与实现11在写法上更好的改进版,注意说的不是性能速度,而是易记与使用上。和它的两个前辈都是零毫秒级别的,以后就用这个来工作与吓人。
a-z ↩︎