基本概念
js正则有两种写法,两种构建方式一致,第二种看起来简洁建议使用第二种,而且第二种效率更高些
let regex = new RegExp('xyz', 'i');
let regex = /xyz/i; // xyz是匹配内容,i是修饰符
正则对象有如下属性
regex.ignoreCase; // true,表示是否设置了i(忽略大小写)修饰符
regex.global; // false, 表示是否设置了g(全局匹配)修饰符
regex.multiline; // false, 表示是否设置了m(多行模式)修饰符
regex.unicode; // false, 表示是否设置了u(Unicode 模式)修饰符
regex.sticky; // false, 表示是否设置了y(粘连)修饰符
regex.dotAll; // false, 表示是否设置了s(dotAll)修饰符
regex.lastIndex; // 0, 返回一个数值,表示下一次开始搜索的位置,该属性可读写
regex.source; // xyz, 返回正则表达式的字符串形式(不包括反斜杠),该属性只读
regex.flags; // i, 返回正则表达式的修饰符
正则对象的相关方法
test方法
返回一个布尔值, 表示当前模式是否能匹配参数字符串
regex.test('xy'); // false
regex.test('xyz rede'); // true
如果正则设置了g修饰符,test方法每次都从上一次匹配位置开始匹配,如果匹配失败则从头匹配
let r = /x/g;
let s = '_x_x';
...
// 第一次执行
r.lastIndex; // 0
r.test(s); // true
...
// 第二次执行
r.lastIndex; // 2
r.test(s); // true
...
// 第三次执行
r.lastIndex; // 4
r.test(s); // false
...
// 第四次执行
r.lastIndex; // 0
r.test(s); // true
// 还可以主动设置匹配开始位置,从开始位置进行匹配
r.lastIndex = 4;
r.test(s); // false
exec方法
返回匹配结果(返回的是类数组,可以使用Array.from转成数组)
let s = '_x_x';
let r1 = /x/;
let r2 = /y/;
r1.exec(s) // ["x", index: 1, input: "_x_x", groups: undefined]
r2.exec(s) // null
匹配结果的index是整个模式匹配成功的开始位置(从0开始计数),input是原字符串
r1.exec(s).index; // 1
r1.exec(s).input; // "_x_x"
如果匹配的正则是组匹配(包含圆括号),则返回的结果包含组匹配的结果
let r3 = /_(x)/;
Array.from(r3.exec(s)); // ["_x", "x"]
// 用下面的例子加强对这个问题的理解
let r1 = /a(b+)a/;
let r2 = /ab+a/;
let r3 = /b+/;
let s = '_abbba_aba_';
Array.from(r1.exec(s)); // ["abbba", "bbb"]
Array.from(r2.exec(s)); // ["abbba"]
Array.from(r3.exec(s)); // ["bbb"]
// 与test方法类似,如果设置了g修饰符,也可以通过修改lastIndex设置开始搜索的位置
字符中与正则相关方法
match
返回一个数组,成员是所有匹配的子字符串
// 与exec方法类似,如果不加全局匹配,两者结果一致
let s = '_x_x';
let r1 = /_(x)/;
s.match(r1); // ["_x", "x", index: 0, input: "_x_x", groups: undefined]
r1.exec(s); // ["_x", "x", index: 0, input: "_x_x", groups: undefined]
如果加了g修饰符,进行全局匹配,两者结果不同
// match会返回所有匹配到的结果;而exec会返回本次匹配到的结果
r1 = /_(x)/g;
s.match(r1); // ["_x", "_x"]
r1.exec(s); // ["_x", "x", index: 0, input: "_x_x", groups: undefined]
// 正则表达式的lastIndex属性,对match方法无效
search
返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1(不受是否有g修饰符的影响)
let s = '_x_x';
let r1 = /_(x)/;
s.search(r1); // 0
replace
替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容
let s = '_x_x';
let r1 = /_x/;
// 如果不加g修饰符,只会匹配第一个字符
s.replace(r1, 'y'); // "y_x"
...
// 如果添加了g修饰符,则匹配全部
r1 = /_x/g;
s.replace(r1, 'y'); // "yy"
第二个参数可以使用$ 符指代替换内容
s = '0_xa1_xb';
r1 = /_x/g;
$&用来表示匹配的内容
s.replace(r1, '$&+');
// "0_x+a1_x+b", _x是匹配文字,所以字符串中_x替换成_x+
$`匹配结果前面的文本
s.replace(r1, '$`+');
// "00+a10_xa1+b",匹配第一个_x时匹配的前面文本是0,匹配第二个_x时匹配的前面文本是0_xa1,所以结论就是0(0+)a1(0_xa1+)b <前面括号只是表述转换过程,方便理解>
$’匹配结果后面的文本,使用时注意加转义字符
s.replace(r1, '$\'+'); // "0a1_xb+a1b+b",匹配第一个_x时,其后面的文本a1_xb,匹配第二个_x时,其后面的文本时b,所以结论就是0(a1_xb+)a1(b+)b <前面括号只是表述转换过程,方便理解>
$n匹配成功的第n组内容,n是从1开始的自然数(针对组匹配匹配出的内容)
'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1'); // “world hello”, $1是指第一个(\w+)匹配的内容,也就是hello,$2是第二个(\w+)匹配的内容,也就是world,所以最终结论是world hello
// 第二个参数还可以函数,用来特殊处理参数
'hello world'.replace(/(\w+)\s(\w+)/, function (match) {
return match.toUpperCase();
}); // "HELLO WORLD"
// 同时这个函数还可以接收$n的参数,用来表示匹配组匹配的内容
'hello world'.replace(/(\w+)\s(\w+)/, function (match, $1, $2) {
return `${$1.toUpperCase()} ${$2}`;
}); // "HELLO world"
split
按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组
// 不使用正则
'a, b,c, d'.split(','); // ["a", " b", "c", " d"]
// 使用正则,使用正则默认进行的全局匹配,匹配后进行分割,组成新的数组
'a, b,c, d'.split(/,/); // ["a", " b", "c", " d"], 这个规则是匹配','
'a, b,c, d'.split(/, /); // ["a", " b,c", "d"], 这个规则匹配的是', '
'a, b,c, d'.split(/, */); // ["a", "b", "c", "d"], 这个规则匹配是','或者', '
修饰符
g 修饰符
表示全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果
let regex = /b/;
let str = 'abba';
regex.exec(str); // ["b", index: 1, input: "abba", groups: undefined], 多次运行,我们会发现index的值一直都是1,并不会向后继续匹配
regex = /b/g; // 加上g修饰符
regex.exec(str); // ["b", index: 1, input: "abba", groups: undefined]
regex.exec(str); // ["b", index: 2, input: "abba", groups: undefined]
regex.exec(str); // null, 我们看到加了g以后,会从上一次结果开始匹配
i 修饰符
表示忽略大小写
/abc/.test('ABC'); // false
/abc/i.test('ABC'); // true
这个忽略大小写会在某些情况下会因编码不同出错
'\u004B'和'\u212A',都是大写K
/[a-z]/i.test('\u212A'); // false
/[a-z]/i.test('\u004B'); // true
可以借助u修饰符来识别非规范的编码K
/[a-z]/iu.test('\u212A'); // true
m 修饰符
表示多行模式(multiline)
默认情况下(即不加m修饰符时),^和$匹配字符串的开始处和结尾处,加上m修饰符以后,^和$还会匹配行首和行尾,即^和$会识别换行符(\n)
/world$/.test('hello world\n'); // false
/world$/m.test('hello world\n'); // true
u 修饰符
使正则表达式能正确处理四个字节的UTF-16编码
/^\uD842/.test('\uD842\uDFB7'); // true, '\uD842\uDFB7'是一个字符'𠮷',ES5会把这个识别成两个字符,所以匹配为true
/^\uD842/u.test('\uD842\uDFB7'); // false, 可以正常识别
由于ES6新增了大括号表示Unicode字符,所以针对这种新的模式,也需要添加u修饰符才能正确识别
/\u{61}/.test('a'); // false '\u{61}'就是'a'
/\u{61}/u.test('a'); // true
针对码点大于0xFFFF的字符进行相关操作都要加上u修饰符要不无法正确识别
/𠮷{2}/.test('𠮷𠮷'); // false
/𠮷{2}/u.test('𠮷𠮷'); // true
/^\S$/.test('𠮷'); // false
/^\S$/u.test('𠮷'); // true
正确返还字符串长度的函数
function codePointLength(text) {
var result = text.match(/[\s\S]/gu);
return result ? result.length : 0;
}
let s = '𠮷𠮷';
s.length // 4
codePointLength(s) // 2
y (粘连)修饰符
与g修饰符类似也是全局匹配,不同之处在于y修饰符必须确保匹配从剩余的第一个位置开始
let s = 'aaa_aa_a';
let r1 = /a+/g;
let r2 = /a+/y;
let r3 = /a+_/y;
// g修饰符匹配后,只要后续能符合规则就继续匹配
r1.exec(s); // ["aaa", index: 0, input: "aaa_aa_a", groups: undefined]
r1.exec(s); // ["aa", index: 4, input: "aaa_aa_a", groups: undefined]
r1.exec(s); // ["a", index: 7, input: "aaa_aa_a", groups: undefined]
r1.exec(s); // null
// y修饰符匹配后,如果再次匹配的第一个字符不符合匹配规则就不继续匹配
// 第一次匹配后剩余的字符为'_aa_a',下次进行匹配时不符合正则规则,所以报错
r2.exec(s); // ["aaa", index: 0, input: "aaa_aa_a", groups: undefined]
r2.exec(s); // null
// 第一次匹配后字符为'aa_a',再次匹配符合规则,所以可以继续匹配
r3.exec(s); // ["aaa_", index: 0, input: "aaa_aa_a", groups: undefined]
r3.exec(s); // ["aa_", index: 4, input: "aaa_aa_a", groups: undefined]
r3.exec(s); // null
y修饰符同样遵守lastIndex,不过如果指定的位置不符合匹配条件,y并不会像g一样向后搜索
r3.lastIndex = 3; // 现在指定位置的字符是_ 与r3条件不匹配,所以下面结果为null
r3.exec(s); // null
...
r3.lastIndex = 4; // 现在指定位置的字符是a 能与r3条件匹配
r3.exec(s);
// ["aa_", index: 4, input: "aaa_aa_a", groups: undefined]
...
r1.lastIndex = 3; // g 会向后搜索
r1.exec(s);
// ["aa", index: 4, input: "aaa_aa_a", groups: undefined]
y修饰符实现效果很像是加了^的g修饰符
let r4 = /^a+/g; // 实现效果和r2一致
r4.exec(s); // ["aaa", index: 0, input: "aaa_aa_a", groups: undefined]
r4.exec(s); // null
s 修饰符dotAll模式
. 点代表一切字符,一般情况下点是不能匹配码点大于0xFFFF的字符以及终止符
// 不能识别码点大于0xFFFF的字符可以使用u修饰符来修正
/.{2}/.test('\uD842\uDFB7'); // true, '\uD842\uDFB7'是一个字符,此时.识别有误
/.{2}/u.test('\uD842\uDFB7'); // false, 使用u修饰符后可正常识别
// 如果想要.能匹配终止符可以使用s修饰符
/foo.bar/.test('foo\nbar'); // false, .不匹配\n
/foo.bar/s.test('foo\nbar'); // true
终止符包括:
\n 换行符
\r 回车符
行分隔符
段分隔符(后面两个不知道是代表什么...)
正则的相关匹配规则
- 字面量字符,代表字面含义
/a/ 匹配a
/dog/ 匹配dog
- 元字符,不代表字面含义,匹配的是特殊值
. 匹配除回车(\r),换行(\n),行分隔符(\u2028)和段分隔符(\u2029)以外的所有字符,但是对于码点大于0xFFFF的Unicode字符还是无法正常匹配
/^.$/.test('𠮷'); // false '𠮷'\u{20BB7}
/^.$/u.test('𠮷'); // true 添加u修饰符就可以正确识别
^ 表示字符串的开始位置
/^he/.test('123hello'); // false, 不是以he开头
/he/.test('123hello'); // true, 包含he
$ 表示字符串的结束位置
/lo$/.test('123hello'); // true
/lo$/.test('hello123'); // false
| 表示或者
/lo$|23$/.test('hello123'); // true, 表示要么以lo结尾,要么以23结尾
/lo$ | 23$/.test('hello123'); // 注意不要随意在这些符号前面加空格,会变成另外的一种意思
/lo|23/.test('hello123'); // true
/lo|23/.test('hell o12 3'); // false 选择符是符号前后两者一起筛选,这里匹配的是lo或者23,并不是o或者2
- 转义符
特殊符号要进行转换
+ 在正则中也代表某个模式出现1次或多次,所以如果要匹配加号,需要使用\+
/1\+1/.test('1+1'); // true
需要特殊转义的字符一共如下:
^
.
[
$
(
)
|
*
+
?
{
\\
- 特殊字符
\cX 表示Ctrl-[X],其中的X是A-Z之中任一个英文字母,用来匹配控制字符。
[\b] 匹配退格键(U+0008),不要与\b混淆。
\n 匹配换行键。
\r 匹配回车键。
\t 匹配制表符 tab(U+0009)。
\v 匹配垂直制表符(U+000B)。
\f 匹配换页符(U+000C)。
\0 匹配null字符(U+0000)。
\xhh 匹配一个以两位十六进制数(\x00-\xFF)表示的字符。
\uhhhh 匹配一个以四位十六进制数(\u0000-\uFFFF)表示的 Unicode 字符。
- 字符类示有一系列字符可供选择,只要匹配其中一个就可以了
/[abc]/.test('apple'); // true
^ 如果使用在字符类中会有特殊含义表示是除了指定字符以为的字符,但这个要使用在头部才有意义,否则就是字面意思
/[^abc]/.test('abc'); // false, 除了abc没有其他字符
/[ab^c]/.test('abc'); // true
- 是表示连字符,某些情况下,对于连续序列的字符,连字符(-)用来提供简写形式,表示字符的连续范围
/[a-z]/.test('A'); // false
/[a-z]/.test('z'); // true
a-z: 表示小写字母a-z
A-Z: 表示大写字母A-Z
0-9: 表示数字0-9
- 预定义模式是某些常见模式的简写方式
\d 匹配0-9之间的任一数字,相当于[0-9]
/\d/.test(1); // true
/\d/.test('a'); // false
\D 匹配所有0-9以外的字符,相当于[^0-9]
/\D/.test(1); // false
/\D/.test('a'); // true
\w 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]
/\w/.test(1); // true
/\w/.test(']'); // false
\W 除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9_]
/\W/.test(1); // false
/\W/.test(']'); // true
\s 匹配空格(包括换行符、制表符、空格符等),相等于[ \t\r\n\v\f]
/\s/.test(' '); // true, 匹配空格
\S 匹配非空格的字符,相当于[^\t\r\n\v\f]
/\S/.test(' '); // false
\b 匹配词的边界(被匹配的单词要是独立单词,如果是中文也会当做普通英文单词处理)
/\bworld/.test('hello world') // true
/\bworld/.test('hello-world') // true
/\bworld/.test('helloworld') // false,
/\b你/.test('hello 你 world'); // false
/\b你/.test('hello world你'); // true
/\b你好/.test('hello world你好'); // true
\B 匹配非词边界,即在词的内部
/\B你/.test('hello 你 world'); // true
- 精确匹配,使用大括号({})表示。{n}表示恰好重复n次,{n,}表示至少重复n次,{n,m}表示重复不少于n次,不多于m次
/lo{2}k/.test('look') // true
/lo{2,5}k/.test('looook') // true
// 量词符用来设定某个模式出现的次数,是某些精确匹配的简写形式
? 等同{0,1},表示至少出现0次但不多于1次
* 等同于{0,},表示至少出现0次
+ 等同于{1,},表示至少出现1次
// 量词符(+, *)默认是贪婪模式匹配,会尽可能的匹配更多字符,直到不能匹配
'aa abb'.match(/ab+/); // ["abb"], 这里更多的去匹配b
'aa abb'.match(/ab+?/); // ['ab'], 这里只匹配了一个b,看起来和?量词类似
'aa abb'.match(/ab?/); // ["a", index: 0, input: "aa abb", groups: undefined], 从这可以看出?本身就是非贪婪匹配,所以index:0也就是开头a,已经符合匹配条件,停止匹配
'aa abb'.match(/a*/); // ["aa"]
'aa abb'.match(/a*?/); // [""], *一样是贪婪模式,按/a*/的正则规则,空字符已经符合条件
组匹配
正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容
'fredfred'.match(/(fred)+/);
// ["fredfred", "fred", index: 0, input: "fredfred", groups: undefined] 会包含分组内容
'fredfred'.match(/(fred)+/g); // ["fredfred"] 当使用了全局匹配会忽略分组匹配的内容
(?:x)称为非捕获组,表示不返回该组匹配的内容,即匹配的结果中不计入这个括号
'fredfred'.match(/(?:fred)+/);
// ["fredfred", index: 0, input: "fredfred", groups: undefined] 不包含分组内容
断言
先行断言
x(?=y) 称为先行断言(Positive look-ahead),x只有在y前面才匹配,y不会被计入返回结果
// 比如下面的字符,如果你想只匹配%前的数字
'23 34%'.match(/\d+/g); // ["23", "34"]
'23 34%'.match(/\d+(?=%)/g); // ["34"]
后行断言
(?<=y)x 后行断言,与先行断言正好相反,x只有在y后面才匹配
'23 34% %90 %a'.match(/(?<=%)\d+/g); // ["90"] 只匹配在%后的数字
后行断言的组匹配与正常情况不一样
// 一般的正则是从左到右匹配
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]
...
// 后行断言是从右到左匹配
/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]
先行否定断言
x(?!y) 称为先行否定断言(Negative look-ahead),x只有不在y前面才匹配,y不会被计入返回结果
// 还是上面的字符,此时你不想匹配%前的数字
'23 34%'.match(/\d+(?!%)/g); // ["23", "3"]
后行否定断言
(?<!y)x 后行否定断言,与先否定行断言相反,x只有不在y后面才匹配
'23 34% %90 %a'.match(/(?<!%)\d+/g); // ["23", "34", "0"] 只匹配不在%后的数字
具名组匹配
ES2018 引入了具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用
const date = '1999-12-31';
const r1 = /(\d{4})-(\d{2})-(\d{2})/;
“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”, 比如:
?<year>、?<month>
const r2 = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
r1.exec(date); // ["1999-12-31", "1999", "12", "31", index: 0, input: "1999-12-31", groups: undefined]
r2.exec(date); // ["1999-12-31", "1999", "12", "31", index: 0, input: "1999-12-31", groups:{year: "1999", month: "12", day: "31"}]
r2.exec(date).groups; // {year: "1999", month: "12", day: "31"}
利用解构赋值可以很方便的从匹配结果中取值
let {groups: {year, day}} = r2.exec(date); // year: 1999, day: 31
在字符串替换时,使用$<组名>引用具名组会使代码更有意义
date.replace(r2, '$3 $2 $1') // 31 12 1999
date.replace(r2, '$<day> $<month> $<year>'); // 31 12 1999
// 在正则表达式中引用匹配结果也可以使用具名组匹配使代码看着更易明白
/^([a-z]+)!\1$/.test('abc!abc'); // true \1是分组匹配的结果,这个正则是表示!前后数据一样
/^(?<word>[a-z]+)!\k<word>$/.test('abc!abc'); // true 使用具名组后,表达式含义更明显