摘要:老姚《JS正则迷你书(1.1版)》笔记
牢记
正则表达式是匹配模式,要么匹配字符,要么匹配位置
正则可视化网站:Regulex
一、字符匹配攻略
1、两种模糊匹配
横向模糊匹配是指一个正则可匹配的字符串的长度不是固定的,其实现的方式是使用量词,像 /ab{2,5}c/
纵向模糊匹配是指一个正则匹配的的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以是多种可能,像 /a[123]b/
2、字符组
字符组表示在同一个位置可能出现的各种字符,在中括号 [] 中列出所有可能出现的字符,只匹配其中一个字符。像 [abc] 表示匹配的可以是 a 或 b 或 c 三者之一
纵向模糊匹配有时要排除某些情况,字符组的第一位放 ^(脱字符),表示求反。像 [^abc] 表示 a、b、c 之外的任意一个字符
常见的简写形式:
字符组 | 具体含义 |
---|---|
\d | 表示 [0-9]。表示是一位数字 记忆方式:其英文是 digit(数字) |
\D | 表示 [^0-9]。表示除数字外的任意字符 |
\w | 表示 [0-9a-zA-Z_]。表示数字、大小写字母和下划线 记忆方式:w 是 word 的简写,也称单词字符 |
\W | 表示 [^0-9a-zA-Z_]。非单词字符 |
\s | 表示 [ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符 记忆方式:s 是 space 的首字母,空白符的单词是 white space |
\S | 表示 [^ \t\v\n\r\f]。 非空白符 |
. | 表示 [^\n\r\u2028\u2029]。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符这四个行终结符除外 记忆方式:想想省略号 ... 中的每个点,都可以理解成占位符,表示任何类似的东西 |
匹配任意字符: 使用 [\d\D]、[\w\W]、[\s\S] 或 [^] 都可以
3、量词
量词也称重复
量词 | 具体含义 |
---|---|
{m, n} | 表示连续出现最少 m 次,最多 n 次 |
{m,} | 表示至少出现 m 次 |
{m} | 等价于 {m,m},表示出现 m 次 |
? | 等价于 {0,1},表示出现或者不出现 记忆方式:问号的意思表示,有吗? |
+ | 等价于 {1,},表示出现至少一次 记忆方式:加号是追加的意思,得先有一个,然后才考虑追加 |
* | 等价于 {0,},表示出现任意次,有可能不出现 记忆方式:看看天上的星星,可能一颗没有,可能零散有几颗,可能数也数不过来 |
4、贪婪匹配与惰性匹配
贪婪模式下会尽可能多的匹配,有多少匹配多少。但是有时候贪婪模式不能满足我们的需求,这时在量词后面加个问号就能实现惰性匹配
贪婪量词 | 惰性量词 |
---|---|
{m, n} | {m, n}? |
{m,} | {m,}? |
? | ?? |
+ | +? |
\* | \*? |
5、多选分支
多选分支可以支持多个子模式任选其一,用 |(管道符)分隔,表示其中任何之一。像 (p1|p2|p3) 这样 p1、p2、p3 是子模式
分支结构是惰性的
1 | var regexA = /good|goodbye/g; |
6、案例分析
1) 匹配 16 进制颜色值
要求匹配:
1 | #ffbbad |
分析:
表示一个 16 进制的字符,可以使用字符组 [0-9a-fA-F]
出现 3 或 6 次,可以使用量词和分支结构
要注意分支结构是惰性的,出现 6 次的情况要先判断
1 | var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g; |
2)匹配时间
要求匹配:
1 | // 24 小时制 |
分析:
时间因该是从 00:00 到 23:59,共 4 位数字
第一位是 [01] 时,第二位可以是 [0-9] ;第一位是 [2] 时,第二位可以是 [0-3]
第三位是 [0-5],第四位是 [0-9]
1 | var regex = /^([01][0-9]|2[0-3]):[0-5][0-9]$/; |
如果还要匹配 “7:9” 这种省略 0 的格式
1 | var regex = /^(0?[0-9]|1[0-9]|2[0-3]):(0?[0-9]|[1-5][0-9])$/; |
3)匹配日期
要求匹配:
1 | // yyyy-mm-dd 格式 |
分析:
年 可用 [0-9]{4}
月 共12个月份 01-12,当第一位是 0 的时候,第二位是 [1-9];当第一位是 1 的时候,第二位是 [0-2]。可用 (0[1-9]|1[0-2])
日 最大31天 01-31,可用 (0[1-9]|[12][0-9]|3[01])
1 | var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/; |
window 操作系统文件路径
要求匹配:1
2
3
4F:\study\javascript\regex\regular expression.pdf
F:\study\javascript\regex\
F:\study\javascript
F:\分析:
整体的结构是盘符:\文件夹\文件夹\文件夹
盘符 可用[a-zA-Z]:\\
,盘符不区分大小写,\
需要转义
文件夹\ 名字不能包含一些特殊字符,而且至少要有一个字符,还可以出现任意次,可用 ([[^\:<>|”?\r\n/]+\])
文件夹 可以没有最后的\
可用 ([^\:*<>|”?\r\n/]+)?1
2
3
4
5var regex = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/; // true
console.log( regex.test("F:\\study\\javascript\\regex\\regular expression.pdf") ); // true
console.log( regex.test("F:\\study\\javascript\\regex\\") ); // true
console.log( regex.test("F:\\study\\javascript") ); // true
console.log( regex.test("F:\\") ); // true匹配 id
要求从<div id="container" class="main"></div>
中提取 id=”container”
分析:
主要部分是获取两个双引号之间的内容,可用 [^”]*1
2
3var regex = /id="[^"]*"/
var string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]); // id="container"
二、位置匹配攻略
位置(锚)是相邻字符之间的位置
ES5 中共有 6 种锚: ^ 、 $ 、 \b 、 \B 、 (?=p) 、 (?!p)
1、^ 和 $
^ (脱字符)匹配开头,在多行匹配中(即有修饰符 m)时匹配行开头
$ (美元符号)匹配结尾,在多行匹配中匹配行结尾
2、\b 和 \B
\b 是单词边界,包括 \w 与 \W 、 ^ 、 $ 之间的位置
\B 是非单词边界,与 \b 相反,去掉 \b 后都是 \B,包括 \w 与 \w 和 \W 与 \W 、 ^ 、 $ 之间的位置
3、(?=p) 和 (?!p)
(?=p) 正向先行断言,其中的 p 是一个子模式,表示 p 的前面那个位置,也就是说该位置后必须符合 p
(?!p) 负向先行断言,其与 (?=p)相反,表示非 p 前面的位置
4、位置特性
可以将位置理解成一个或多个空字符 “”
比如 “hello” 等价于如下的形式:
1 | "hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "" + "o" + ""; |
字符之间的位置,可以写成多个
1 | "hello" == "" + "" + "" + "h" + "" + "" + "e" + "" + "l" + "" + "l" + "" + "o" + ""; |
示例:
1 | var resultA = /^^hello$$$/.test("hello"); |
5、相关案例
不匹配任何东西的正则
分析:
匹配不存在可能性的正则,比如匹配字符位于 ^ 之前或 $ 之后的字符串,这样的字符串是不存在的1
/.^/ /$./
数字的千位分隔符表示法
要求:1
"12345678" => "12,345,678"
分析:
要把相应位置替换成,
,从后向前每三位数字添加一个,
,匹配最后三个数字可用 \d{3}$ ,则匹配最后三个数字前面的位置可用 (?=\d{3}$)
三个数字一组,而且至少要有一组,否则就不需要添加分隔符 可用 (?=(\d{3})+$)
当数字的位数是3的倍数时,开头不需要添加分隔符,要除去开头位置 可用 (?!^)(?=(\d{3})+$)1
2
3
4
5var regex = /(?!^)(?=(\d{3})+$)/g;
var resultA = "12345678".replace(regex, ',');
var resultB = "123456789".replace(regex, ',');
console.log(resultA); // "12,345,678"
console.log(resultB); // "123,456,789"数字千分位分隔符加强表示法
要求:1
"12345678 123456789" => "12,345,678 123,456,789"
分析:
此时的位置不是从字符结尾开始向前匹配,而是从边界开始,将 ^ 与 $ 替换成 \b 即可 (?!\b)(?=(\d{3})+b)
\b 是单词边界, (?!\b) 是非单词边界,即 \B 。最终得到 \B(?=(\d{3})+\b)1
2
3
4var string = "12345678 123456789",
var regex = /\B(?=(\d{3})+\b)/g;
var result = string.replace(regex, ',')
console.log(result); // "12,345,678 123,456,789"货币格式化
要求:1
"1888" => "$ 1,888.00"
分析:
需要将原来的数字保留两位小数,添加千分位分隔符,还要在前面添加美元符号1
2
3var num = 1888;
var str = num.toFixed(2).replace(/\B(?=(\d{3})+\b)/g,",").replace(/^/,"$$ "); // replace 第二个参数中特殊字符 $$ 代表美元符号
console.log(str) // "$ 1,888.00"验证密码问题
要求:
密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符
分析:
只考虑前两个条件很容易满足 [0-9a-zA-z]{6,12}
如果至少包括一种字符,比如数字 可用 (?=.*[0-9])1
var regex = /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/;
重点是 (?=.[0-9])^ 这部分,它由 (?=.[0-9]) 和 ^ 两部分组成,即开头前面还有一个位置。类比空字符串,相当于同时要满足这两个要求的位置:这个位置后面至少要有一个数字同时还要必须是行开头。
注意:在匹配字符时基本都是只匹配一个要求,但匹配位置时,可以同时匹配多个要求(类比多个空字符 “” )
同时包含数字和小写字符两种 可用 (?=.[0-9])(?=.[a-z])^[0-9A-Za-z]{6,12}$
类似的1
var regex = /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[A-Z])(?=.*[a-z]))^[0-9A-Za-z]{6,12}$/;
另一种思路:至少包括 2 种字符即从开头到尾不能只有数字、不能只有小写字符、不能只有大写字符
比如不能只用数字时1
var regex = /(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/;
不能都是小写字符、不能都是大写字符也是类似,可得结果
1
var regex = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;
三、括号的作用
括号提供了分组,便于我们引用它。有两种情形来引用某个分组:
1、在 JavaScript 里引用它
2、在正则表达式里引用它
1、分组
在 /(ab)+/ 中,括号提供分组功能,使 + 作用于 ab 这个整体
1 | var regex = /(ab)+/g; |
2、分支结构
在多选分支结构 (p1|p2) 中,提供了分支表达式的所有可能
1 | var regex = /^I (like|love) fruit$/; |
如果去掉括号 /^I like|love fruit$/ ,它匹配的是 “I like” 和 “love fruit”
3、分组引用
括号的重要作用之一,可以用于数据的提取及替换
正则引擎在匹配的过程中,给每个分组都开辟一个空间,用来存储每个分组匹配到的数据
- 提取数据
比如提取年、月、日match 是字符串的方法,其返回的是一个数组。全局匹配时,返回所有匹配到的字符串(“2019-08-04”),但不会返回捕获组,或者未匹配 null ;非全局匹配时,未匹配返回 null,有匹配则仅返回第一个完整匹配及其相关的捕获组,第一个元素是整体匹配结果(“2019-08-04”),然后是各个分组(括号里)匹配的内容(“2019”, “08”, “04”)。还有几个属性:匹配的下标(index: 0),搜索的字符串(input: “2019-08-04”),命名捕获组(groups: undefined) ,undefined 说明没有定义命名捕获组1
2
3
4
5var regex = /(\d{4})-(\d{2})-(\d{2})/;
var regexp = /(\d{4})-(\d{2})-(\d{2})/g;
var string = "2019-08-04 2019-08-05";
console.log(string.match(regex)); // ["2019-08-04", "2019", "08", "04", index: 0, input: "2019-08-04 2019-08-05", groups: undefined]
console.log(string.match(regexp)); // ["2019-08-04", "2019-08-05"]
search 用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。如果匹配成功,则返回正则表达式在字符串中首次匹配项的索引;否则,返回 -1。
exec 是正则实例对象的方法,其返回的是一个数组,它对应字符串的 match 方法。非全局匹配时,它与 match 的返回值一模一样。但是全局匹配时则有些不同: match 返回的是所有匹配到的项, exec 返回的与非全局匹配时返回的内容保持一致,返回第一个完整匹配及其相关的捕获组。但再次调用 exec 时,会返回第二个完整匹配及其相关的捕获组。全局匹配时,lastIndex 是其开始匹配位置的索引,默认为 0 ,匹配成功后 lastIndex 的值将会变为上一次匹配文本之后的第一个字符的位置索引。lastIndex 的值可以手动修改,如果在成功地匹配了某个字符串之后就开始检索另一个新的字符串,需要手动地把这个属性设置为 0 。不具有标志 g 和不表示全局模式的 RegExp 对象不能使用 lastIndex 属性,而且字符串的正则方法里 lastIndex 属性是不起作用的。
test 是正则实例对象的方法,用于检测一个字符串是否匹配某个模式,它对应字符串的 search 方法。如果字符串中有匹配的值返回 true ,否则返回 false 。当方法 exec 或 test 再也找不到可以匹配的文本时,它们会自动把 lastIndex 属性重置为 0
1 | var regex = /(\d{4})-(\d{2})-(\d{2})/; |
也可以使用构造函数的全局属性 $1 至 $9 来获取:
1 | var regex = /(\d{4})-(\d{2})-(\d{2})/; |
- 替换
如果想把 yyyy-mm-dd 格式,替换成 mm/dd/yyyy 怎么做replace 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。1
2
3
4
5
6
7
8
9
10
11
12var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2019-08-17";
// 方法一
var result1 = string.replace(regex, "$2/$3/$1"); // 使用全局属性指代相应的分组
// 方法二
var result2 = string.replace(regex, function () {
return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
// 方法三
var result3 = string.replace(regex, function (match, year, month, day) {
return month + "/" + day + "/" + year;
});
语法str.replace(regexp|substr, newSubStr|function)
第一个参数可以是字符串或者正则对象。当参数是字符串时,参数被视为一整个字符串,而不是一个正则表达式,且仅第一个匹配项会被替换。当参数是正则对象或者其字面量时,该正则所匹配的内容会被第二个参数的返回值替换掉
第二个参数可以是用于替换掉第一个参数在原字符串中的匹配部分的字符串或者一个用来创建新子字符串的函数。如果第二个参数是函数,函数的返回值将替换掉第一个参数匹配到的结果。如果第一个参数是正则表达式,并且其为全局匹配模式(g),那么这个函数将被多次调用,每次匹配都会被调用。函数传入的参数顺序:先是匹配到的子串,然后是捕获到的各个分组,接着是匹配到的子串在原字符串中的索引,最后是原字符串
4、反向引用
在正则本身里引用之前出现的分组
1 | var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/; |
其中 \1 表示引用第一个分组匹配到的内容,类似的可以使用 \2 \3 … \10 来引用之前匹配到的相应分组, \10 表示匹配到的第10个分组。如果要匹配 \1 和 0 ,要使用 (?:\1)0 或者 \1(?:0)
如果引用了不存在的分组,比如 \2 就匹配 “\2”,\2 表示对 “2” 进行了转义
1 | var regex = /\1\2\3\4\5\6\7\8\9/; |
如果有括号嵌套,一律以左括号为准
1 | var regex = /^((\d)(\d(\d)))\1\2\3\4$/; |
如果分组后面有量词的话,分组捕获到的是最后一次的匹配,因而反向引用也是如此
1 | var regex = /(\d)+ \1/; |
5、非捕获括号
捕获括号都会捕获它们匹配到的数据以便后续引用,因此也被称为捕获型分组和捕获型分支。如果只想要括号的最原始功能,并不引用它,此时可以使用非捕获括号 (?:p) 和 (?:p1|p2|p3),注意它与 (?=p) 不同
1 | var regex = /(?:ab)+/g; |
6、相关案例
字符串 trim 方法模拟
trim 方法是去掉字符串的开头和结尾的空白符。有两种方法实现:
1、匹配到开头和结尾的空白符,然后替换成空字符;
2、匹配需要的整个字符串,然后用引用提取相应数据1
2
3
4
5
6
7
8
9var str = " string ";
function trim1(s) {
return s.replace(/^\s+|\s+$/g,"");
}
function trim2(s) {
return s.replace(/^\s*(.*?)\s*$/, "$1");
}
console.log(trim1(str)) // "string"
console.log(trim2(str)) // "string"方法 2 使用了惰性匹配 *? ,否则也会匹配到最后一个空格之前所有的空格
将每个单词的首字母转换为大写
思路:找到每个单词的首字母进行替换1
2
3
4
5
6function titleize(str) {
return str.toLowerCase().replace(/(?:^|\s)\w/g, function(s) {
return s.toUpperCase();
})
}
console.log(titleize("I love fruit")) // "I Love Fruit"驼峰化
思路:单词的界定是,前面的字符可以是多个连字符、下划线以及空白符1
2
3
4
5
6function camelize(str) {
return str.replace(/[-_ ]+(.)?/g, function(match, s) {
return s ? s.toUpperCase() : "";
})
}
camelize('-webkit--transform ') // "WebkitTransform"(.) 用于获取单词首字母, ? 用于匹配字符串末尾不是单词字符的情况比如空格” “
中划线化
思路: 驼峰化的逆过程,可能原本字符串就包含多种分隔符。分两步走:先统一分隔符,然后将字符串全部转换为小写1
2
3
4function dasherize(str) {
return str.replace(/([A-Z])/g, "-$1").replace(/[-_ ]+/g, "-").toLowerCase();
}
console.log(dasherize('Webkit Transform')); // "-webkit-transform"HTML 转义和反转义
HTML 中的预留字符必须被替换为字符实体 HTML 字符实体
转义思路:匹配到要转义的字符进行替换即可1
2
3
4
5
6
7
8
9
10
11
12
13function escapeHTML(str) {
var escapeChars = {
'<': 'lt',
'>': 'gt',
'"': 'quot',
'&': 'amp',
'\'': '#39'
}
return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') + ']', 'g'), function (match) {
return '&' + escapeChars[match] + ';';
});
}
console.log(escapeHTML('<div>I love fruit</div>')); // "<div>I love fruit</div>"反转义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function unescapeHTML(str) {
var htmlEntities = {
nbsp: ' ',
lt: '<',
gt: '>',
quot: '"',
amp: '&',
apos: '\''
};
return str.replace(/&([^;]+);/g, function(match, key) {
if(key in htmlEntities) {
return htmlEntities[key]
}
return match;
});
}
console.log(unescapeHTML('<div>I love fruit</div>')); // "<div>I love fruit</div>"匹配成对标签
思路:匹配开标签可以用 <[^>]+> ,匹配闭标签可以用 </[^>]+> 。但是成对的标签要用反向引用 </\1> ,并且开标签要提供捕获组1
2
3
4var regex = /<([^>]+)>[\d\D]*<\/\1>/;
console.log(regex.test("<div>I love fruit</div>")); // true
console.log(regex.test("<p>I love fruit</p>")); // true
console.log(regex.test("<div> I love fruit </p>")); // false<([^>]+)> 提供捕获组供闭标签反向引用。 [\d\D] 匹配任意字符,同样的还有 [\w\W]、[\s\S] 和 [^]
四、回溯法原理
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。 —— 百度百科
JavaScript 的正则引擎是 NFA。相对于 DFA ,NFA 有回溯匹配慢但编译快
回溯原因
贪婪量词
js 正则匹配默认是贪婪的,尽可能的从多往少的方向尝试。当多个贪婪量词挨着存在并相互有冲突时,先下手为强,优先满足前面的,深度优先搜索1
2
3var str = "12345";
var regex = /(\d{1,3})(\d{1,3})/;
console.log(str.match(regex)); // ["12345", "123", "45", index: 0, input: "12345", groups: undefined]惰性量词
惰性量词就是在贪婪量词后面加个问号。表示尽可能少的匹配,但也会有回溯的现象1
2
3var str = "12345";
var regex = /^(\d{1,3}?)(\d{1,3})$/;
console.log(str.match(regex)); // ["12345", "12", "345", index: 0, input: "12345", groups: undefined]
匹配过程中发现前面只匹配一个字符不满足条件,只能再添加一个,因而 \d{1,3}? 匹配的是 12分支结构
分支也是惰性的1
2
3var str = "candy";
var regex = /^(?:can|candy)$/;
console.log(regex.test(str)); // true
第 4 步匹配结尾失败,第 5 步返回尝试下一个分支,可以认为是一种回溯
贪婪量词“试”的策略是:买衣服砍价。价钱太高了,便宜点,不行,再便宜点。
惰性量词“试”的策略是:卖东西加价。给少了,再多给点行不,还有点少啊,再给点。
分支结构“试”的策略是:货比三家。这家不行,换一家吧,还不行,再换。
五、正则表达式的拆分
1、结构
JavaScript 正则表达式中的结构:字符字面量、字符组、量词、锚、分组、选择分支、反向引用
结构 | 说明 |
---|---|
字面量 | 匹配一个具体字符,包括不用转义的和需要转义的。比如 a 匹配字符 "a", 又比如 \n 匹配换行符,又比如 \. 匹配小数点。 |
字符组 | 匹配一个字符,可以是多种可能之一,比如 [0-9],表示匹配一个数字。 也有 \d 的简写形式。 另外还有反义字符组,表示可以是除了特定字符之外任何一个字符,比如 [^0-9], 表示一个非数字字符,也有 \D 的简写形式。 |
量词 | 表示一个字符连续出现,比如 a{1,3} 表示 "a" 字符连续出现 3 次。 另外还有常见的简写形式,比如 a+ 表示 "a" 字符连续出现至少一次。 |
锚 | 匹配一个位置,而不是字符。比如 ^ 匹配字符串的开头,又比如 \b 匹配单词边界,又比如 (?=\d) 表示数字前面的位置。 |
分组 | 用括号表示一个整体,比如 (ab)+,表示 "ab" 两个字符连续出现多次,也可以使用非捕获分组 (?:ab)+。 |
分支 | 多个子表达式多选一,比如 abc|bcd,表达式匹配 "abc" 或者 "bcd" 字符子串。反向引用,比如 \2,表示引用第 2 个分组。 |
2、操作符
操作符的优先级从上至下,由高到低:
操作符描述 | 操作符 | 优先级 |
---|---|---|
转义符 | \ | 1 |
括号和方括号 | (...)、(?:...)、(?=...)、(?!...)、[...] | 2 |
量词限定符 | {m}、{m,n}、{m,}、?、*、+ | 3 |
位置和序列 | ^、$、\元字符、一般字符 | 4 |
管道符(竖杠) | | | 5 |
3、注意要点
匹配字符串整体问题
因为是要匹配整个字符串,我们经常会在正则前后中加上锚 ^ 和 $。 比如匹配 “abc” 或者 “bcd” 时写成/^abc|bcd$/
,因为位置字符和字符序列优先级要比竖杠高,所以它匹配的是 ^abc 或者 bcd$,而不是 ^abc$ 或者 ^bcd$。正确的是/^(abc|bcd)$/
量词连缀问题
要匹配这样的字符串:1、字符为 a 、 b 、 c 三选一;2、字符串长度为 3 的倍数
[abc] 满足第一条,添加上量词 {3} 为 /^[abc]{3}+$/。这样两个量词连着使用会爆错,其实后面的 + 修饰的是前面整个分组,因而要这样写 /^([abc]{3})+$/元字符转义问题
元字符,就是正则中有特殊含义的字符,用到的有 20 个: ^、$、.、*、+、?、|、\、/、(、)、[、]、{、}、=、!、:、- 、,
当匹配上面的字符本身时,可以一律转义1
2
3var str = "^$.*+?|\\/()[]{}=!:-,";
var regex = /\^\$\.\*\+\?\|\\\/\(\)\[\]\{\}\=\!\:\-\,/;
console.log(regex.test(str)); // true但并不是每个字符都要转义,具体要看情况:
1、跟字符组相关的元字符有 [、]、^、-,在会引起歧义的地方进行转义1
2
3var str = "^$.*+?|\\/[]{}=!:-,";
var regex = /[\^$.*+?|\\/\[\]{}=!:\-,]/g; // 匹配其中一个,而非反义字符组
console.log(str.match(regex)); // ["^", "$", ".", "*", "+", "?", "|", "\", "/", "[", "]", "{", "}", "=", "!", ":", "-", ","]2、匹配 “[abc]” 和 “{3,5}”
只需要在第一个方括号转义即可,因为后面的方括号构不成字符组,不会引起歧义1
2
3var string = "[abc]";
var regex = /\[abc]/g;
console.log(string.match(regex)); // ["[abc]"]3、其它情况
=、!、:、-、, 等符号,只要不在特殊结构中就不需要转义;
小括号前后都要转义:/\(123\)/
;
^、$、.、*、+、?、|、\、/ 等字符,只要不在字符组内,都需要转义
4、案例分析
身份证
正则表达式:/^(\d{15}|\d{17}[\dxX])$/
两种情况:1、15位数字;2、17位数字外加 x 或 X 或一位数字。IPV4 地址
要求匹配的范围是 0.0.0.0 到 255.255.255.255,并且可以匹配用”0”补齐的3位
正则表达式是:/^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/
整个结构可分为((...)\.){3}(...)
,其中 (…) 就是匹配的 0(00、000) 到 255 这个范围的数字,这样就很好分析了
六、正则表达式的构建
从四点来构建一个合适的正则表达:
1、平衡法则
2、构建正则前提
3、准确性
4、效率
1、平衡法则
要做到几点的平衡:
1、匹配预期的字符串
2、不匹配非预期的字符串
3、可读性和可维护性
4、效率
2、构建正则前提
有几点要考虑:
1、是否能使用正则?
2、是否有必要使用正则?能用字符串 API解决的简单问题,就不要使用正则
3、是否有必要构建一个复杂的正则?比如第二章验证密码,要求密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符。使用正则 /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/
,可以使用多个小正则来做:
1 | var regex1 = /^[0-9A-Za-z]{6,12}$/; |
先判断是不是由 6-12 位数字和大小写字母构成,然后再依次判断是不是只包含一种字符返回结果
3、准确性
匹配预期目标,不匹配非预期目标
匹配固定电话
类似这样的格式:1
2
3055112345678
0551-12345678
(0551)12345678由区号和号码两部分组成
区号由”0”开头的 3 到 4 位数字组成:0\d{2,3}
号码由非”0”开头的 7 到 8 位数字组成:[1-9]\d{6,7}
号码基本一致,而区号形式多变,这些情况是或的关系,可以构建分支匹配多样的区号:0\d{2,3}-?|\(0\d{2,3}\)
。最后得到:/^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/
浮点数
匹配以下格式:1
2
31.23、+1.23、-1.23
10、+10、-10
.2、+.2、-.2可以分为三部分:
符号部分[+-]
整数部分\d+
小数部分\.\d+
在不考虑细节问题时得到/^[+-]?(\d+)?(\.\d+)?$/
或者分情况讨论:
匹配 “1.23”、”+1.23”、”-1.23”:/^[+-]?([1-9]\d*|0)\.\d+$/
匹配 “10”、”+10”、”-10”:/^[+-]?([1-9]\d*|0)$/
匹配 “.2”、”+.2”、”-.2”:/^[+-]?\.\d+$/
三者是或的关系,最后得到:/^[+-]?(([1-9]\d*|0)\.\d+|([1-9]\d*|0)|\.\d+))$/
解决思路: 针对每种情形,分别写出正则,然用分支把它们合并在一起,再提取分支公共部分,就能得到准确的正则。
4、效率
保证了准确性后,才需要是否要考虑要优化
正则表达式的运行分为如下的阶段:
1、编译;
2、设定起始位置;
3、尝试匹配;
4、匹配失败的话,从下一位开始继续第 3 步;
5、最终结果:匹配成功或失败。
匹配会出现效率问题,主要出现在上面的第 3 阶段和第 4 阶段
常用方法:
- 使用具体型字符组来代替通配符,来消除回溯。比如匹配双引用号之间的字符,使用
/"[^"]*"/
替换掉通配符/".*?"/
- 使用非捕获型分组。如果不需要使用分组引用和反向引用时,可以使用非捕获分组 (?:)
- 独立出确定字符。比如
/a+/
可以修改成/aa*/
,多确定了字符 “a”,可以加快判断是否匹配失败,进而加快移位的速度 - 提取分支公共部分。比如
/this|that/
修改成/th(?:is|at)/
,可以减少匹配过程中可消除的重复 - 减少分支的数量,缩小它们的范围。比如
/red|read/
可以修改成/rea?d/
,但这样可读性会降低
七、正则表达式编程
1、正则表达式的四种操作
正则表达式要先“匹配”,就是要先“查找”,然后才能进行验证、切分、提取、替换等操作
2、验证
匹配之后,根据匹配结构进行判断的操作即“验证”
相关 API 的验证操作:
1 | var regex = /\d/; |
3、切分
把目标字符串按照一定规则切成一段一段的称为切分
1 | var string = "html,css,javascript"; |
4、提取
有时我们需要获取匹配到的部分数据,通常会使用分组引用(分组捕获)功能再配合相关 API 来获取数据。比如获取年月日:
1 | var string = "2019-09-17"; |
还有 replace:
1 | var string = "2019-09-17"; |
5、替换
一般使用 replace 进行替换
1 | var string = "2019-09-17"; |
6、相关 API 要点
用于正则操作的方法,共有 6 个,字符串实例 4 个,正则实例 2 个:
1 | String#search |
search 和 match 的参数问题
search 和 match 会把字符串转换为正则1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var string = "2019.09.21";
// search
console.log(string.search(".")); // 0
//需要修改成下列形式之一
console.log(string.search("\\.")); // 4
console.log(string.search(/\./)); // 4
// match
console.log(string.match(".")); // ["2", index: 0, input: "2019.09.21", groups: undefined]
//需要修改成下列形式之一
console.log(string.match("\\.")); // [".", index: 4, input: "2019.09.21", groups: undefined]
console.log(string.match(/\./)); // [".", index: 4, input: "2019.09.21", groups: undefined]
// split
console.log(string.split(".")); // ["2019", "09", "21"]
// replace
console.log(string.replace(".", "/")); // "2019/09.21"match 返回结果的格式问题
1
2
3
4
5var string = "2019.09.21";
var regex1 = /\b(\d+)\b/;
var regex2 = /\b(\d+)\b/g;
console.log(string.match(regex1)); // ["2019", "2019", index: 0, input: "2019.09.21", groups: undefined]
console.log(string.match(regex2)); // ["2019", "09", "21"]没有 g,返回的是标准匹配格式,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然后是整体匹配的第一个下标,接着是输入的目标字符串,最后是命名的捕获组;
有 g,返回的是所有匹配的内容;
当没有匹配时均返回 nullexec 比 match 更强大
match 有 g 时,返回信息很少,但 exec 方法能接着上一次匹配后继续匹配1
2
3
4
5
6
7
8
9
10var string = "2019.09.21";
var regex2 = /\b(\d+)\b/g;
console.log(regex2.exec(string)); // ["2019", "2019", index: 0, input: "2019.09.21", groups: undefined]
console.log(regex2.lastIndex); // 4
console.log(regex2.exec(string)); // ["09", "09", index: 5, input: "2019.09.21", groups: undefined]
console.log(regex2.lastIndex); // 7
console.log(regex2.exec(string)); // ["21", "21", index: 8, input: "2019.09.21", groups: undefined]
console.log(regex2.lastIndex); // 10
console.log(regex2.exec(string)); // null
console.log(regex2.lastIndex); // 0其中正则实例 lastIndex 属性,表示下一次匹配开始的位置。
在使用 exec 时,经常需要配合使用 while 循环:1
2
3
4
5
6
7
8
9var string = "2019.09.21";
var regex2 = /\b(\d+)\b/g;
var result;
while (result = regex2.exec(string)) {
console.log(result, regex2.lastIndex);
}
// ["2019", "2019", index: 0, input: "2019.09.21", groups: undefined] 4
// ["09", "09", index: 5, input: "2019.09.21", groups: undefined] 7
// ["21", "21", index: 8, input: "2019.09.21", groups: undefined] 10修饰符 g,对 exex 和 test 的影响
字符串的四个方法,每次匹配时,lastIndex 都是从 0 开始的,即 lastIndex 属性始终不变;
而正则实例的两个方法 exec、test,当正则是全局匹配时,每一次匹配完成后,都会修改 lastIndex。1
2
3
4var regex = /a/g;
console.log(regex.test("a"), regex.lastIndex); // true 1
console.log(regex.test("aba"), regex.lastIndex); // true 3
console.log(regex.test("ababc"), regex.lastIndex); // false 0注意第三次匹配是从下标 3 开始的,自然找不到
如果没有 g,就都是从字符串第 0 个字符处开始尝试匹配:1
2
3
4var regex = /a/;
console.log(regex.test("a"), regex.lastIndex); // true 0
console.log(regex.test("aba"), regex.lastIndex); // true 0
console.log(regex.test("ababc"), regex.lastIndex); // true 0test 整体匹配时需要使用 ^ 和 $
test 是看目标字符串中是否有子串匹配正则,即部分匹配,如果要整体匹配就要要添加开头和结尾:1
2
3console.log(/123/.test("a123b")); // true
console.log(/^123$/.test("a123b")); // false
console.log(/^123$/.test("123")); // truesplit 相关注意事项
split 有两点要注意:
第一,它可以有第二个参数,表示结果数组最大长度:1
2var string = "html,css,javascript";
console.log(string.split(/,/, 2)); // ["html", "css"]第二,正则使用分组时,结果数组中是包含分隔符的:
1
2
3var string = "html,css,javascript";
console.log(string.split(/(,)/)); // ["html", ",", "css", ",", "javascript"]
console.log(string.split(/(c)/)); // ["html,", "c", "ss,javas", "c", "ript"]replace 是很强大的
replace 的第二个参数,可以是字符串,也可以是函数
当第二个参数是字符串时,如下的字符有特殊的含义:
例如,把 "2,3,5",变成 "5=2+3":属性 描述 $1,$2,...,$99 匹配第 1-99 个 分组里捕获的文本 $& 匹配到的子串文本 $` 匹配到的子串的左边文本 $' 匹配到的子串的右边文本 $$ 美元符号 例如,把 "2,3,5",变成 "222,333,555":1
2var result = "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2");
console.log(result); // "5=2+3例如,把 "2+3=5",变成 "2+3=2+3=5=5":1
2var result = "2,3,5".replace(/(\d+)/g, "$&$&$&");
console.log(result); // "222,333,555"需要把 "=" 替换成 "=2+3=5=",其中 $& 匹配的是 =, $` 匹配的是 2+3, $' 匹配的是 5,整体替换下来就是 $&$`$&$'$&1
2var result = "2+3=5".replace(/=/, "$&$`$&$'$&");
console.log(result); // "2+3=2+3=5=5"
当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么:
1 | "1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (match, $1, $2, index, input) { // 函数参数依次是:匹配的子串,所有捕获组,子串起始索引,原字符串 |
使用构造函数需要注意的问题
优先使用字面量,因为用构造函数会多写很多 \1
2
3
4var string = "2019-09-23 2019.09.23 2019/09/23";
var regex = /\d{4}(-|\.|\/)\d{2}\1\d{2}/g;
// regex = new RegExp("\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}", "g");
console.log(string.match(regex)); // ["2019-09-23", "2019.09.23", "2019/09/23"]修饰符
正则实例对象也有相应的只读属性:修饰符 描述 g 全局匹配,即找到所有匹配的,单词是 global。 i 忽略字母大小写,单词是 ingoreCase。 m 多行匹配,只影响 ^ 和 $,二者变成行的概念,即行开头和行结尾。单词是 multiline。 1
2
3
4var regex = /\w/img;
console.log(regex.global); // true
console.log(regex.ignoreCase); // true
console.log(regex.multiline); // truesource 属性
正则实例对象属性,除了 global、ingnoreCase、multiline、lastIndex 属性之外,还有一个 source属性。在构建动态的正则表达式时,可以通过查看 source 属性,来确认构建出的正则到底是什么:1
2
3var className = "high";
var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
console.log(regex.source); // "(^|\s)high(\s|$)"构造函数属性
构造函数的静态属性基于所执行的最近一次正则操作而变化。除了 $1,…,$9 还有:静态属性 描述 简写形式 RegExp.input 最近一次目标字符串 RegExp["$_"] RegExp.lastMatch 最近一次匹配的文本 RegExp["$&"] RegExp.lastParen 最近一次捕获的文本 RegExp["$+"] RegExp.leftContext 目标字符串中lastMatch之前的文本 RegExp["$`"] RegExp.rightContext 目标字符串中lastMatch之后的文本 RegExp["$'"] 1
2
3
4
5
6
7
8
9
10
11
12
13var regex = /([abc])(\d)/g;
var string = "a1b2c3d4e5";
string.match(regex);
console.log(RegExp.input); // a1b2c3d4e5
console.log(RegExp["$_"]); // a1b2c3d4e5
console.log(RegExp.lastMatch); // c3
console.log(RegExp["$&"]); // c3
console.log(RegExp.lastParen); // 3
console.log(RegExp["$+"]); // 3
console.log(RegExp.leftContext); // a1b2
console.log(RegExp["$`"]); // a1b2
console.log(RegExp.rightContext); // d4e5
console.log(RegExp["$'"]); // d4e5
7、相关案例
使用构造函数生成正则表达式
一般优先使用字面量来创建正则,但有时正则表达式的主体是不确定的,此时可以使用构造函数来创建。比如模拟 getElementsByClassName 方法1
2
3
4
5
6
7
8
9
10
11
12function getElementsByClassName(className) {
var elements = document.getElementsByTagName("*");
var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
var result = [];
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (regex.test(element.className)) {
result.push(element)
}
}
return result;
}if 语句中使用正则替代 &&
模拟 ready 函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var readyRE = /complete|loaded|interactive/;
function ready(callback) {
if (readyRE.test(document.readyState) && document.body) {
callback()
}
else {
document.addEventListener(
'DOMContentLoaded',
function () {
callback()
},
false);
}
};
ready(function () {
alert("加载完毕!")
});使用强大的 replace
可以使用 replace 匹配到的信息来做些事情。比如查询字符串(querystring)压缩技术:1
2
3
4
5
6
7
8
9
10
11
12function compress(source) {
var keys = {};
source.replace(/([^=&]+)=([^&]*)/g, function (full, key, value) {
keys[key] = (keys[key] ? keys[key] + ',' : '') + value;
});
var result = [];
for (var key in keys) {
result.push(key + '=' + keys[key]);
}
return result.join('&');
}
console.log(compress("a=1&b=2&a=3&b=4")); // "a=1,3&b=2,4"