深入解析 JavaScript 正则表达式的用法,包括创建方法(构造函数与字面量)、元字符、修饰符、量词、分组匹配、断言等高级特性。
正则表达式是一组由字母和符号组成的特殊文本,它可以用来从文本中找出满足你想要的格式的句子。
一个正则表达式是一种从左到右匹配主体字符串的模式。 “Regular expression” 这个词比较拗口,我们常使用缩写的术语 “regex”或“regexp” 。 正则表达式可以从一个基础字符串中根据一定的匹配模式替换文本中的字符串、验证表单、提取字符串等等。
JS中正则表达式的创建
构造函数创建
把正则表达式包裹在
//之间
const reg = /\d+/g;
字面量创建
通过调用RegExp对象的构造函数
const reg = new RegExp("\d+","g");
第一个参数可以是一个变量,遇到特殊字符``需要使用\进行转义
元字符
| 元字符 | 描述 |
|---|---|
| . (英语句号) | 查找单个字符,除了换行和行结束符 |
| \w | 查找数字、字母及下划线 |
| \W | 查找非单词字符 |
| \d | 查找数字 |
| \D | 查找非数字字符 |
| \s | 查找空白字符 |
| \S | 查找非空白字符 |
| \b | 匹配单词边界 |
| \B | 匹配非单词边界 |
| \0 | 查找 NULL 字符 |
| \n | 查找换行符 |
| \f | 查找换页符 |
| \r | 查找回车符 |
| \t | 查找制表符 |
| \v | 查找垂直制表符。 |
| \xxx | 查找以八进制数 xxx 规定的字符 |
| \xdd | 查找以十六进制数 dd 规定的字符 |
| \uxxxx | 查找以十六进制数 xxxx 规定的 Unicode 字符 |
修饰符
| 修饰符 | 描述 |
|---|---|
| i | 执行对大小写不敏感的匹配 |
| g | 执行全局匹配(查找所有匹配而非在找到第一个匹配后停止) |
| m | 执行多行匹配 |
| u | ES6在正则中添加了u修饰符,用来正确处理大于\uFFFF的 Unicode 字符 |
注意: /g全局匹配所导致bug,考虑去掉全局配或者重置正则对象为的尾索引为0:reg.lastIndex=0
量词
| 量词 | 描述 |
|---|---|
| n+ | 匹配任何包含 至少一个(零个或多个) n 的字符串。 例如,/a+/ 匹配 "candy" 中的 "a","caaaaaaandy" 中所有的 "a" |
| n* | 匹配任何包含 零个或多个 n 的字符串。 例如,/bo*/ 匹配 "A ghost booooed" 中的 "boooo","A bird warbled" 中的 "b",但是不匹配 "A goat grunted" |
| n? | 匹配任何包含 零个或一个 n 的字符串。 例如,/e?le?/ 匹配 "angel" 中的 "el","angle" 中的 "le" |
| n{X} | 匹配包含 X 个 n 的序列的字符串。 例如,/a{2}/ 不匹配 "candy," 中的 "a",但是匹配 "caandy," 中的两个 "a",且匹配 "caaandy." 中的前两个 "a" |
| n{X,} | X 是一个正整数。前面的模式 n 连续出现至少 X 次时匹配。 例如,/a{2,}/ 不匹配 "candy" 中的 "a",但是匹配 "caandy" 和 "caaaaaaandy." 中所有的 "a" |
| n{X,Y} | X 和 Y 为正整数。前面的模式 n 连续出现至少 X 次,至多 Y 次时匹配。 例如,/a{1,3}/ 不匹配 "cndy",匹配 "candy," 中的 "a","caandy," 中的两个 "a",匹配 "caaaaaaandy" 中的前面三个 "a"。注意,当匹配 "caaaaaaandy" 时,即使原始字符串拥有更多的 "a",匹配项也是 "aaa" |
| n$ | 匹配任何结尾为 n 的字符串 |
| ^n | 匹配任何开头为 n 的字符串 |
方括号[]
方括号用于查找某个范围内的字符:
| 表达式 | 描述 |
|---|---|
| [abc] | 查找方括号之间的任何字符 |
| [^abc] | 查找任何不在方括号之间的字符 |
| [0-9] | 查找任何从 0 至 9 的数字 |
| [a-z] | 查找任何从小写 a 到小写 z 的字符 |
| [A-Z] | 查找任何从大写 A 到大写 Z 的字符 |
| [A-z] | 查找任何从大写 A 到小写 z 的字符 |
| [adgk] | 查找给定集合内的任何字符 |
| [^adgk] | 查找给定集合外的任何字符 |
| [.?!] | 匹配标点符号(.或?或!) |
| [\u4e00-\u9fa5] | 中文范围 |
| (red|blue|green) | 查找任何指定的选项(|为或,表示符合任意一个都可以) |
分组匹配
分组主要是用过
()进行实现的,(...)中包含的内容将会被看成一个整体。 比如beyond{3},是匹配d字母3次。而(beyond){3}是匹配beyond三次。在()内使用|达到或的效果,如(abc | xxx)可以匹配abc或者xxx。
引用分组
这是括号一个重要的作用,有了它,我们就可以进行数据提取,以及更强大的替换操作。
比如提取出年、月、日,可以这么做:
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let string = "2017-06-12";
console.log( string.match(regex) );
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
match返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。(注意:如果正则是否有修饰符g , match返回的数组格式是不一样的)。
另外也可以使用正则对象的exec方法:
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let string = "2017-06-12";
console.log( regex.exec(string) );
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
同时,也可以使用构造函数的全局属性 $1至 $9来获取:
let regex = /(\d{4})-(\d{2})-(\d{2})/;
let string = "2017-06-12";
regex.test(string); // 正则操作即可,例如
//regex.exec(string);
//string.match(regex);
console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"
反向引用
除了使用相应API来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。
还是以日期为例。比如要写一个正则支持匹配如下三种格式:
2016-06-122016/06/122016.06.12
一开始想到这个:
let regex = /\d{4}[-/.]\d{2}[-/.]\d{2}/;
let string1 = "2017-06-12";
let string2 = "2017/06/12";
let string3 = "2017.06.12";
let string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // true
假设我们想要求分割符前后一致怎么办?此时需要使用反向引用:
let regex = /\d{4}([-/.])\d{2}\1\d{2}/;
let string1 = "2017-06-12";
let string2 = "2017/06/12";
let string3 = "2017.06.12";
let string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false
注意里面的\1,表示的引用之前的那个分组([-/.])。不管它匹配到什么(比如-),\1都匹配那个同样的具体某个字符。
我们知道了\1的含义后,那么\2和\3的概念也就理解了,即分别指代第二个和第三个分组。
括号嵌套:
let regex = /^((\d)(\d(\d)))\1\2\3\4$/;
let string = "1231231233";
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3
- 第一个字符是数字,比如说
1, - 第二个字符是数字,比如说
2, - 第三个字符是数字,比如说
3, - 接下来的是
\1,是第一个分组内容,那么看第一个开括号对应的分组是什么,是123, - 接下来的是
\2,找到第2个开括号,对应的分组,匹配的内容是1, - 接下来的是
\3,找到第3个开括号,对应的分组,匹配的内容是23, - 最后的是
\4,找到第3个开括号,对应的分组,匹配的内容是3。
\10呢?\10是表示第10个分组,而不是\1和0
let regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
let string = "123456789# ######"
console.log( regex.test(string) );
// => true
引用不存在的分组:
因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身。
例如\2,就匹配"\2"。注意"\2"表示对"2"进行了转义。
let regex = /\1\2\3\4\5\6\7\8\9/;
console.log( regex.test("\1\2\3\4\5\6\7\8\9") );
console.log( "\1\2\3\4\5\6\7\8\9".split("") );
非捕获分组
上面出现的分组,都会捕获它们匹配到的数据,以便后续引用,因此也称他们是捕获型分组。
如果只想要括号最原始的功能,但不会引用它,即,既不在API里引用,也不在正则里反向引用。此时可以使用非捕获分组
(?:p)
let string = '2024-5-6'
let regex = /(?:\d{4})-(\d{1,2})-(\d{1,2})/
// 只捕获月份和日期,不捕获年份
regex.test(string); // 正则操作即可,例如
console.log(RegExp.$1); // "5"
console.log(RegExp.$2); // "6"
console.log(RegExp.$3); // 空的,啥也没有捕获到
字符串trim() 方法模拟:
function trim(str) {
return str.replace(/^\s+|\s+$/g, '');
}
console.log( trim(" foobar ") );
// => "foobar"
function trim(str) {
return str.replace(/^\s*(.*?)\s*$/g, "$1");
}
console.log( trim(" foobar ") );
// => "foobar"
?的用法
?直接跟随在子表达式后面
这种方式是最常用的用法,具体表示匹配前面的1次或者0次,类似于{0,1} ,如:
abc(d)?可匹配abc和abcd
let str = 'abcdabcdddabc'
let reg3 = /abc[d]?/g
console.log(str.match(reg3)); // [ 'abcd', 'abcd', 'abc' ]
let reg2 = /abc(d){0,1}/g
console.log(str.match(reg2)); // [ 'abcd', 'abcd', 'abc' ]
let reg1 = /abc(d)?/g
console.log(str.match(reg1)); // [ 'abcd', 'abcd', 'abc' ]
非贪婪匹配
关于贪婪和非贪婪,贪婪匹配的意思是,在同一个匹配项中,尽量匹配更多所搜索的字符,非贪婪则相反。
正则匹配的默认模式是贪婪模式
当“?”紧跟在如下任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的
let str = "AAAAAaAAAAAAAaAa"
console.log(str.match(/\S+a/g));
// ['AAAAAaAAAAAAAaAa']
str.match(/\S+?a/g)
console.log(str.match(/\S+?a/g));
//[ 'AAAAAa', 'AAAAAAAa', 'Aa' ]
非捕获分组
当我们使用正则表达式的时候,捕获的字符串会被缓存起来以供后续使用,具体表现为每个
()中的表达式所匹配到的内容在进行正则匹配的过程中,都会被缓存下来。
()表示捕获分组:()会把每个分组里的匹配的值保存起来,使用RegExp.$n可以查看(n是一个数字,表示第n个捕获组的内容)(?: )表示非捕获分组:和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来,也就是匹配冒号后的内容但不获取匹配结果,不进行存储供以后使用。
let reg1 = /(a+)(b*)c/;
let str = 'abcdabcdddabc'
console.log(reg1.test(str)); //true
console.dir(RegExp);
let reg2 = /(a+)(?:b*)c/;
let str = 'aaabcdabbbbcdddabc'
console.log(reg2.test(str)); /true
console.dir(RegExp);
断言
只是负责判断某个位置左/右侧是否符合要求,这种结构被称为断言——只匹配位置
也称先行断言和后行断言为环视,也有人叫预搜索
| 前瞻 | (?=pattern) exp | 查找pattern前面的exp 非获取匹配,正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。 |
|---|---|---|
| 负前瞻 | (?!pattern) exp | 查找非pattern的前面的exp 非获取匹配,正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。 |
| 后顾 | (?<= pattern) exp | 查找pattern后面的exp 非获取匹配,反向肯定预查,与正向肯定预查类似,只是方向相反。 |
| 负后顾 | (?<!= pattern) exp | 查找非pattern后面的exp 非获取匹配,反向否定预查,与正向否定预查类似,只是方向相反。 |
"中国人".replace(/中国(?=人)/, "法国") // 法国人
"中国人".replace(/(?<=中国)人/, "rr") // 中国rr 匹配中国人中的人,将其替换为rr
"法国人".replace(/(?<=中国)人/, "rr") // 法国人 因为人前面不是中国,所以无法匹配到
// 前瞻 pattern(?=exp)
"中国人".replace(/中国(?=人)/, function () {
console.log(arguments); // [Arguments] { '0': '中国', '1': 0, '2': '中国人' }
})
// 负前瞻 pattern(?=exp)
"中国人".replace(/中国(?!=狗)/, function () {
console.log(arguments); // [Arguments] { '0': '中国', '1': 0, '2': '中国人' }
})
// 后顾 (?<= exp)pattern
"中国人".replace(/(?<=中国)人/, function () {
console.log(arguments); //[Arguments] { '0': '人', '1': 2, '2': '中国人' }
})
// 负后顾 (?<!= exp)pattern
"中国人".replace(/(?<!=法国)人/, function () {
console.log(arguments); // [Arguments] { '0': '人', '1': 2, '2': '法国人' }
})
先行断言
从左往右看
正向先行断言
(?=表达式),指在某个位置向右看,表示所在位置右侧 必须能匹配表达式
例如:(?=.*?[a-z])(?=.*?[A-Z]).+ 这段正则表达式规定了匹配的字符串中必须包含至少一个大写和小写的字母。(常用于密码强度验证)
反向先行断言
(?!表达式)的作用是保证右边 不能出现 某字符。
例如:\w+@(?!qq)\w+.\w+匹配不是qq邮箱的数据。<((?!p)\w+)>.*?</\1>匹配除了<p></p>标签之外的HTML标签
后行断言
从右往左看
正向后行断言
(?<=表达式),指在某个位置向左看,表示所在位置左侧必须能匹配表达式
例如:如果要取出喜欢两个字,要求喜欢的前面有我,后面有你,这个时候就要这么写:(?<=我)喜欢(?=你)。
反向后行断言
(?<!表达式),指在某个位置向左看,表示所在位置左侧不能匹配表达式
例如:如果要取出喜欢两个字,要求喜欢的前面没有我,后面没有你,这个时候就要这么写:(?<!我)喜欢(?!你)。
匹配方法
RegExp 对象与属性
RegExp 对象方法
| 方法 | 描述 |
|---|---|
| compile | 在 1.5 版本中已废弃。 编译正则表达式 |
| test | 检索字符串中指定的值。返回 true 或 false |
| exec | 检索字符串中指定的值。返回找到的值,并确定其位置 |
| toString | 返回正则表达式的字符串 |
let reg = /\d{1,5}/
let flag = reg.test("幸运数字:888")
console.log(flag) //true
var srt = "中国电信10010, 中国移动10086,中国联通10000"
var reg = /\d{5}/g
var arr = reg.exec(srt)
while (arr != null) {
// console.log(arr, "\n")
console.log(arr[0], reg.lastIndex)
arr = reg.exec(srt)
}
/*
10010 9
10086 20
10000 30
*/
RegExp 对象属性
| 属性 | 描述 |
|---|---|
| constructor | 返回一个函数,该函数是一个创建 RegExp 对象的原型 |
| lastIndex | 用于规定下次匹配的起始位置 ⚠️ 注意: 该属性只有设置标志 g 才能使用 |
| source | 返回正则表达式的匹配模式 |
| global | 判断是否设置了 "g" 修饰符 |
| ignoreCase | 判断是否设置了 "i" 修饰符 |
| multiline | 判断是否设置了 "m" 修饰符 |
注意: /g全局匹配所导致bug,考虑去掉全局配或者重置正则对象为的尾索引为0:reg.lastIndex=0
字符串方法
| 方法 | 描述 |
|---|---|
| match | 一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返回 null。 |
| matchAll | 一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)。 |
| search | 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。 |
| replace | 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。 |
| split | 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String方法。 |
match
str.match(regexp)方法在字符串str中找到匹配regexp的字符
如果 regexp 不带有 g 标记,则它以数组的形式返回第一个匹配项,其中包含分组和属性 index(匹配项的位置)、input(输入字符串,等于 str)
let str = "I love JavaScript";
let result = str.match(/Java(Script)/);
console.log( result[0] ); // JavaScript(完全匹配)
console.log( result[1] ); // Script(第一个分组)
console.log( result.length ); // 2
// 其他信息:
console.log( result.index ); // 7(匹配位置)
console.log( result.input ); // I love JavaScript(源字符串)
如果
regexp带有 g 标记,则它将所有匹配项的数组作为字符串返回,而不包含分组和其他详细信息
let str = "I love JavaScript";
let result = str.match(/Java(Script)/g);
console.log( result[0] ); // JavaScript
console.log( result.length ); // 1
matchAll
返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器
const regexp = /t(e)(st(\d?))/g;
const str = 'test1test2';
const array = [...str.matchAll(regexp)];
console.log(array[0]);
// expected output: Array ["test1", "e", "st1", "1"]
console.log(array[1]);
// expected output: Array ["test2", "e", "st2", "2"]
search
返回第一个匹配项的位置,如果未找到,则返回-1
let str = "A drop of ink may make a million think";
console.log( str.search( /ink/i ) ); // 10(第一个匹配位置)
replace
替换与正则表达式匹配的子串,并返回替换后的字符串。在不设置全局匹配g的时候,只替换第一个匹配成功的字符串片段
const reg1=/javascript/i;
const reg2=/javascript/ig;
console.log('hello Javascript Javascript Javascript'.replace(reg1,'js'));
//hello js Javascript Javascript
console.log('hello Javascript Javascript Javascript'.replace(reg2,'js'));
//hello js js js
split
使用正则表达式(或子字符串)作为分隔符来分割字符串
console.log('12, 34, 56'.split(/,\s*/)) // 数组 ['12', '34', '56']