Fork me on GitHub

也许你正则的基础并没有那么好

  stay hungry, stay foolish.

  其实在之前的博客当中曾经做过一篇高阶的正则操作,但是最近发现自身的正则基础其实挺弟弟的,所以希望借这篇夯实一下。

填坑

量词

  量词其实语法非常简单。但,概念性的东西如果没有经过仔细考究,就容易跟别的一些概念混淆…

  量词本身是作为修饰存在的,它与前面紧跟着的匹配内容组合使用。比如在/\d\w+/中,+就是仅作用在\w的匹配上。

  那我们看有哪些量词:
  +:匹配前面正则内容至少1次(>=1),它等价于{1,}
  *:匹配前面正则内容任意次(>=0),它等价于{0,}
  ?:匹配前面正则内容0次或1次(0|1),它等价于{0, 1}。在别的编程语言中?同时也代表着贪婪匹配(在js中就是没有?量词限制的正则)的对立面,即匹配尽量少的字符。这里的贪婪可能还是比较抽象,下面我会先聊一个之前弄混的特殊字符.,然后以一个例子来说明贪婪问题。

  .在正则匹配中的作用是什么呢?它会匹配除换行符之外的任何单个字符。根据官方文档的阐释,首先它不是一个量词,并且只匹配单个字符。

  搞清楚具体的含义后,我们开始讨论?的非贪婪作用模式,默认情况下。像+*这样的量词匹配都是贪婪的,它们会尽可能多地去匹配正则内容,但是当我们在这些量词后面再补上一个?就会使得贪婪匹配变为非贪婪匹配,就以下面这个.*?结合的匹配为例:

1
2
3
4
5
let str = 'some <foo> <bar> new </bar> </foo> thing';
let greedyReg = /<.*>/;
greedyReg.exec(str)[0]; // '<foo> <bar> new </bar> </foo>'
let nonGreedyReg = /<.*?>/;
nonGreedyReg.exec(str)[0]; // '<foo>'

  更多的非贪婪结合场景:
  x*?
  x+?
  x??
  x{n}?
  x{n,}?
  x{n,m}?

集合

  集合是啥,通常我将[匹配元素]称之为集合,对于使用集合匹配来说,它也只能匹配这个集合范围内的一个元素,但是通过设置前面讨论的量词就可以匹配一整段内容。

  大致整理使用模式如下:
  [012345abcdefg]:全量枚举。
  [a-dA-D0-9]:设定范围。
  /[a-z.]+/:量词结合。PS:关键字符在集合中是无需转义的
  [\b]这个比较特殊一点,它会匹配一个退格(U+0008),区别于匹配边界的元字符\b

边界匹配

匹配单词边界

  我们通过使用\b进行单词边界的匹配,那怎么样才算是边界呢?像/\w\b\w/这样的肯定就不算,因为这个\b的前后都有字符跟随,那它肯定不能被定义成一个边界。边界的正确定义应当是\b匹配的内容长度为0。

  好像还是很抽象,那就拿MDN上的例子来讲。现在我们要对moon进行边界匹配:

  1. /\bm/就是一个正确的边界匹配示例,它可以匹配m
  2. /oon\b/同样也是一个单词边界匹配,它可以匹配oon
  3. /oo\b/moon来说就不是一个单词边界匹配,因为实际oo还跟着n

  综上,我们大概可以有个比较清晰的认知了,除了前面的定义外,也可以理解为\b匹配对应位置的前后都没有别的字符连接。

匹配非单词边界

  我们通过使用\B进行非单词边界的匹配,按照MDN上的说法有点难理解,我个人比较倾向于理解成\B是用来匹配前后非空格内容的边界,还是以上面的moon来说,我用/\B.{3}/来匹配最终匹配到的是oon,这就是非单词边界匹配的作用,对moon而言,它的非单词边界从开头算就是m,所以后匹配到的三个字符为oon

  那如果我想匹配on呢,也很简单,在有字符的地方插一个边界即可,/\Bon/

  注:边界匹配后面是不能跟量词的。

^

  ^需要区分使用场景。

非集合场景

  ^用于限制匹配的开头。

集合场景

  ^表明不取集合内容。

相反意义的元字符

对数字

  \d匹配一个数字,等价于[0-9]\D匹配一个非数字,等价于[^0-9]

对单字字符

  \w匹配一个单字字符(数字、字母或者下划线),等价于[A-Za-z0-9_]\W匹配一个非单字字符,等价于[^A-Za-z0-9_]。像%等就要用\W去匹配。

对空白字符

  \s匹配一个空白字符,它包含空格、制表符、换页符和换行符,等价于[\f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]\S匹配一个非空白字符,等价于[^\f\n\r\t\v\u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]

  \f:匹配一个换页符 (U+000C)。
  \n:匹配一个换行符 (U+000A)。
  \r:匹配一个回车符 (U+000D)。
  \t:匹配一个水平制表符 (U+0009)。
  \v:匹配一个垂直制表符 (U+000B)。

捕获组

  在正则表达式中如果我们想额外的匹配一部分,可以使用捕获组来操作,实现方式就是(),还是拿moon举例:

1
2
3
4
5
let str = 'moon';
let regGroup = /mo(on)/;
str.match(regGroup); // ["moon", "on", index: 0, input: "moon", groups: undefined] 捕获整体作为第一个元素返回,第二个则是捕获组内容,其他则是对象属性,数组也不过是对象的一种
regGroup.exec(str); // ["moon", "on", index: 0, input: "moon", groups: undefined]
RegExp.$1; // "on"

非捕获组

  与捕获组对立存在的是非捕获组,我们通过(?:x)的形式来使用这种类型的正则。它与捕获组不同的地方在于,尽管非捕获组内的内容会参与匹配,但不会返回非捕获组中匹配的信息:

1
2
3
4
5
let str = 'moon';
let regGroup = /mo(on)/;
let irregGroup = /mo(?:on)/;
regGroup.exec(str); // ["moon", "on", index: 0, input: "moon", groups: undefined]
irregGroup.exec(str); // ["moon", index: 0, input: "moon", groups: undefined] 非捕获组不会将捕获内容返回
先行断言

  x(?=y):实际匹配x,但是需要具备x后跟着y的条件,即规则生效,但是并不会将这块规则内容纳入到匹配内容中。

后行断言

  (?<=y)x:与先行断言对立,但在括号中写法有所区分?<=其实先后行,理解成实际要捕获的内容在先还是后会更清晰一些,剩余就是括号中的捕获规则写法差异了。

react中dangerouslySetInnerHTML过滤外部样式

  其实就是根据场景过滤html字符串中的会对当前页面造成样式污染的内容,污染内容主要是<head>内的scriptstyle<!-- -->,具体要看引入的HTML结构。实现思路大致是通过replace处理返回一个新字符串,结合第二个回调函数参数,链式处理。

String带的一些方法

replace

  作为字符串替换的API,replace的功能非常强,我们大部分时间使用的仅是其冰山一角(比如第一个参数传字符串,第二个参数用别的字符串替换)。然而私以为真正的精髓在于第一个参数传入正则表达式并且配合第二个参数的回调使用,它几乎可以处理所有的字符串处理场景。开始举例前,还要注意一点:replace的字符串替换并不会修改原值,而是返回一个新的字符串。

寻找字符串中相邻字符最多的字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function findMaxChar(str) {
let char = '', sum = 0;
let reg = /(\w)\1+/g; // 匹配整个字符串内所有符合条件的 \1 配合捕获组使用
str.replace(reg, ($0, $1) => { // $0 全量匹配 $1 捕获组内容
if (sum < $0.length) {
sum = $0.length;
char = $1;
}
})
console.log(`相邻字符出现最多的字符是${char}, 数量为${sum}`);
}

let testStr = 'aaaabcdddddljlljeeckeebbjjijij';
findMaxChar(testStr); // 相邻字符出现最多的字符是d, 数量为5
替换字符串中的匹配内容并且不影响其余内容

  这个描述其实不太好理解,举个例子,还是moon,我现在想把最后的on转大写,但是不影响mo。如果像下面这样操作:

1
2
'moon'.replace(/mo(on)/, () => RegExp.$1.toUpperCase()); // 'ON' 由于最终结果取决于回调的返回结果,这样做仅能改变捕获组的东西还需要我们额外处理
'moon'.replace(/(mo)(on)/, ($0, $1, $2) => $1 + $2.toUpperCase()); // 'moON' 这样虽然可以得到想要的结果,但是用了两个捕获组并且还进行了额外的拼接动作

  那咋整?这里其实最佳体验是使用后行断言来处理,一步到位:

1
2
let replaceReg = /(?<=mo)on/;
moon.replace(replaceReg, str => str.toUpperCase()); // 'moON'

match和matchAll

  注意这是String类型带的方法,而不是RegExp带的方法;matchmatchAll的主要差别在于返回类型上,match会返回以数组格式返回所有的匹配以及捕获组,如果没有匹配到就返回nullmatchAll同样会匹配所有的匹配以及捕获组,但是,它返回的是一个RegExpStringIterator格式的iterator类型,并且是否匹配到都返回这个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let str = 'aabbababbbaaabb';
let reg = /ab/;
str.match(reg); // ["ab", index: 1, input: "aabbababbbaaabb", groups: undefined]
let regG = /ab/g;
str.match(regG); // ["ab", "ab", "ab", "ab"]
str.match('c'); // null
str.matchAll(reg); // RegExpStringIterator {}
[...str.matchAll(reg)].forEach(console.log); // ["ab", index: 1, input: "aabbababbbaaabb", groups: undefined]
[...str.matchAll(regG)].forEach(console.log);
// ["ab", index: 1, input: "aabbababbbaaabb", groups: undefined]
// ["ab", index: 4, input: "aabbababbbaaabb", groups: undefined]
// ["ab", index: 6, input: "aabbababbbaaabb", groups: undefined]
// ["ab", index: 12, input: "aabbababbbaaabb", groups: undefined]
[...str.matchAll('c')].length; // 根据数组长度判断是否存在匹配
let regGroup = /ab(b)/;
str.match(regGroup); // ["abb", "b", index: 1, input: "aabbababbbaaabb", groups: undefined]
[...str.matchAll(regGroup)].forEach(console.log); // ["abb", "b", index: 1, input: "aabbababbbaaabb", groups: undefined]

  从以上的match传入的正则是否带有/g全局匹配符来看,可以得到如果携带/g那返回的数组内不会有index等信息,并且是一个干净的数组,这一点其实应用意义非常大,如:

1
2
3
// 从GMT时间串中,提取返回所有数字时间
// 2018-10-07T11:48:47 Asia/zh-cn => [2018,10,07,11,48,47]
"2018-10-07T11:48:47 Asia/zh-cn".match(/\d{1,}/g)
1
2
// 给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。如`abab`、`abcabc`等。
let testRepeat = s => !!s.match(/^(\w+)\1+$/g);

  search会返回匹配位置的索引(初次匹配的位置,设置global同),无匹配返回-1