听风是风

学或不学,知识都在那里,只增不减。

导航

从零开始学正则(七:终章),详解常用正则API与你可能不知道的正则坑

 壹 ❀ 引

花了差不多半个月的晚上时间,正则入门学习也步入尾声了,当然正则的学习还将继续。不得不说学习成效非常明显,已能看懂大部分正则以及写出不太复杂的正则,比如帮组长写正则验证文件路径正确性,再如进产品页根据页面地址获取产品id:

let pathname = '/webtoprint/dynamicsize-gamebox-2033986.html';
let productId = pathname.match(/\-(\d+)\./)[1]; //2033986

虽然正则都不难,但是相比之前使用split各种切切切真的要舒服的多,想到这一点就开心。所以如果有缘的你看到这篇作为结束的文章,还是非常推荐静下心花点闲余时间学习正则。

那么在最终章,我们将正则与API结合起来,真正把正则用起来,毕竟学了不用总会忘,忘了再学就巨亏。说在前面,正则学习系列文章均为我阅读 老姚《JavaScript正则迷你书》的读书笔记,文中所有正则图解均使用regulex制作。那么本文开始!

 贰 ❀ 正则的四种操作

我们知道正则是一种匹配模式,要么匹配字符,要么匹配位置,正则的核心用处就是在于匹配我们预期的东西。那么拿到了想要的东西能做什么?其实无非就是用于验证,切分,提取以及替换。比如文章开头匹配产品id就是提取我们想要的东西,再如千位分隔符就是将数字替换成我们想要的样子,其实也不难理解,我们来细说这四种操作。

1.验证

谈到验证首先想到的就是表单验证,使用正则验证用户输入字段是否符合规范,比如验证数字密码长度是否为六位,最常用的就是使用test方法:

var regex = /^\d{6}$/;
regex.test(123456); //true
regex.test(12345); //false
regex.test(1234567); //false

除了字符的完整比较验证,有时候我们也需要验证字符中是否包含某个特定字符,这种情况下除了test,我们能用match,search,exec这三个方法来做,比如验证字符中是否包含 ❀ :

可以使用 test 方法:

var regex = /❀/;
regex.test('123❀'); //true
regex.test('a❀b'); //false
regex.test('echo'); //false

使用match方法,此时返回并非布尔值,而是匹配结果,若要判断有没有只需对比返回值是否等于null即可:

var regex = /❀/;
'123❀'.match(regex); //["❀", index: 3, input: "123❀", groups: undefined]
'a❀b'.match(regex); //["❀", index: 1, input: "a❀b", groups: undefined]
'echo'.match(regex); //null

使用search方法,此时返回第一个符合条件字符的索引,若没找到返回-1,所以判断有没有对比是否等于-1即可:

var regex = /❀/;
'❀123❀'.search(regex); //0
'a❀b'.search(regex); //1
'echo'.search(regex); //-1

使用exec方法,此方法结果与match类似,判断有没有看是否为null即可:

var regex = /❀/;
regex.exec('❀123❀'); //["❀", index: 0, input: "❀123❀", groups: undefined]
regex.exec('a❀b'); //["❀", index: 1, input: "a❀b", groups: undefined]
regex.exec('echo'); //null

当然如果真的是判断有没有某个字符的需求,其实用 indexOf 就可以了,这里也只是列举这些方法的作用。

2.切分

说到切分自然想到字符串切分的split方法,比如:

var str = 'hello echo';
var a = str.split(" ");
console.log(a, str); //["hello", "echo"] "hello echo"

split方法返回一个切割后的数组,它并不会修改原字符串。其实split接受参数除了字符意外,它还能接受一个正则作为切分条件,比如上面的例子我们也可以写成:

var str = 'hello echo';
var a = str.split(/\s/);
console.log(a, str); //["hello", "echo"] "hello echo"

3.提取

正如文章开头提取产品id的例子一样,正则提取操作非常使用,而提取就依赖于正则分组存储的特性,所以分组记得一定不能使用非捕获型括号,看个简单的例子:

var regex = /(\d{4})\/(\d{2})\/(\d{2})/;
var result = regex.exec('2019/12/28'); //["2019/12/28", "2019", "12", "28", index: 0, input: "2019/12/28", groups: undefined]
console.log(result[1]); //2019
console.log(result[2]); //12
console.log(result[3]); //28
// 或直接通过RegExp对象访问
console.log(RegExp.$1); //2019
console.log(RegExp.$2); //12
console.log(RegExp.$3); //28

4.替换

替换必须依赖字符串replace方法,比如将yyyy/mm/dd修改为yyyy-mm-dd:

var str = 'yyyy/mm/dd';
var result = str.replace(/\//g, '-');
console.log(result, str); //yyyy-mm-dd yyyy/mm/dd

注意replace返回替换后的字符,并不会修改原字符串。

 叁 ❀ 正则相关的API使用注意

整合上文四种正则操作可以发现,正则使用的API一共就六个,其中字符串方法四个,正则方法两个:

字符串方法split:负责字符串切分,可使用正则作为切分条件,返回切分后的数组,不修改原字符串。

字符串方法search:根据正则查找并返回第一个符合条件的字符索引,注意是第一个,如果没找到返回-1。

字符串方法match:根据正则匹配符合条件的字符,返回一个数组,如果没找到返回null,此方法与exec类似。

字符串方法relpace:根据正则条件进行字符替换,返回替换完成的字符,不会修改源字符串。

正则方法exec:与match类似,返回一个包含符合条件字符,分组匹配字符等信息的数组,没找到返回null。

正则方法test:验证时常用,验证字符是否有符合正则条件的字符,返回一个Boolean值,有为true,没有返回false。

在使用这些方法时,还是有一些需要注意的点,这里我们做个整合:

1.search与match会将字符参数转为正则

字符串的split,replact,match,search四个方法都接受字符串或者正则作为参数,但在使用时,match与search会将字符串参数转为正则

var str = '2019.12.28';
var result1 = str.search('.'); //0
var result2 = str.match('.'); //["2", index: 0, input: "2019.12.28", groups: undefined]

在上面的例子,我们本意是匹配第一个小数点的位置,但这两个方法将小数点转为了正则,也就是通配符,很自然第一个数字2就符合条件,所以search返回了2的索引0,match返回了2。

为了避免这个问题还是建议匹配条件直接使用正则,避免不必要的麻烦,像这样:

var str = '2019.12.28';
var result1 = str.search('\\.'); //4
var result2 = str.match(/\./); //[".", index: 4, input: "2019.12.28", groups: undefined]

当然如果只是查找某个字符有没有,还是推荐indexOf,不用像上面花里胡哨。

2.match匹配受修饰符g影响

我们在前面说字符串方法match与正则方法exec非常类似,若成功匹配都是返回一个数组,失败返回null,而数组中包含了第一个符合条件的字符,分组捕获字符等信息:

var str = '2019.12.28';
var result1 = str.match(/\d+/); //["2019", index: 0, input: "2019.12.28", groups: undefined] 
var result2 = /\d+/.exec('2019.12.28'); //["2019", index: 0, input: "2019.12.28", groups: undefined] 

可以看到不加g情况两个方法匹配结果完全一样,现在我们加上全局匹配修饰符g再看:

var str = '2019.12.28';
var result1 = str.match(/\d+/g); //["2019", "12", "28"]
var result2 = /\d+/g.exec('2019.12.28'); //["2019", index: 0, input: "2019.12.28", groups: undefined]

match方法的正则添加了修饰符g,返回结果将只包含所有符合条件的字符,不再包含字index等信息。而exec方法很明显不受g影响。

3.exec使用修饰符g的妙用

上面说match方法使用了g虽然能拿到所有符合条件的字符,但不知道每次出现的索引信息,exec正好解决了这个问题。

当exec方法使用了修饰符g,exec第一次匹配从索引0开始,之后每次匹配都会从上次匹配失败的索引位置(lastIndex)开始,直至匹配一圈后重置索引为0,也就是开始下一轮匹配:

var str = '2019.12.28';
var regexp = /\d+/g
console.log(regexp.exec('2019.12.28'), regexp.lastIndex); // ["2019", index: 0, input: "2019.12.28", groups: undefined] 4
console.log(regexp.exec('2019.12.28'), regexp.lastIndex); // ["12", index: 5, input: "2019.12.28", groups: undefined] 7
console.log(regexp.exec('2019.12.28'), regexp.lastIndex); // ["28", index: 8, input: "2019.12.28", groups: undefined] 10
console.log(regexp.exec('2019.12.28'), regexp.lastIndex); // null 0
console.log(regexp.exec('2019.12.28'), regexp.lastIndex); // ["2019", index: 0, input: "2019.12.28", groups: undefined] 4

可以看到在走完第四次匹配后,字符串被完整匹配了一遍,此时lastIndex又被重置为0,接下来又开始新一轮匹配。

4.test也会受修饰符g影响

前面说exec会受g影响,准确来说正则的两个方法都受g影响,另一个方法test也是如此,只要添加了全局修饰符g,正则每次匹配完成都会修改lastIndex,这一点与exec保持一致:

var str = '2019.12.28';
var regexp = /\d+/g
console.log(regexp.test('2019.12.28'), regexp.lastIndex); // true 4
console.log(regexp.test('2019.12.28'), regexp.lastIndex); // true 7
console.log(regexp.test('2019.12.28'), regexp.lastIndex); // true 10
console.log(regexp.test('2019.12.28'), regexp.lastIndex); // false 0
console.log(regexp.test('2019.12.28'), regexp.lastIndex); //true 4

5.验证字符整体要加^和$

这个在前面几篇文章已经有说过,一般我们验证用户输入表单信息,都是验证用户输入的完整字符是否符合规格,所以要验证整体是否符合,一定得加^和$:

var regexp = /\d{6}/;
console.log(regexp.test('1234567')); // true
console.log(regexp.test('123456')); // true

var regexp = /^\d{6}$/;
console.log(regexp.test('1234567')); // false
console.log(regexp.test('123456')); // true

比如这个例子中我们要求密码必须是6位数字,如果不加^和$,七位数字也包含了六位数字的情况,所以为true。

6.split你不知道的事

split方法我们在前面说了它接受一个字符或者一个正则作为切分字符的条件,并将字符按此条件切成一个数组并返回。

其实split方法还能接受第二个参数,用于限定返回数组的长度:

var str = '听风是风 时间跳跃 行星飞行 echo';
console.log(str.split(' ', 2)); //["听风是风", "时间跳跃"]

第二点是,如果我们使用正则作为切分条件,且正则使用了分组,那么返回的数据将包含分隔符:

var str = '听风是风 时间跳跃 行星飞行 echo';
console.log(str.split(/(\s)/)); //["听风是风", " ", "时间跳跃", " ", "行星飞行", " ", "echo"]

7.强大的replace方法

我们理解的replace方法第二参数就是自定义字符,最基本的用法:

var str = '听风是风 时间跳跃 行星飞行 echo';
console.log(str.replace(/\s/g,'❀')); //听风是风❀时间跳跃❀行星飞行❀echo

其实正则自身也提供了一部分字符,如下:

属性 描述
$1,$2...$99 匹配第1-99个分组里捕获的文本
$& 匹配到的子串文本
$` 匹配到的子串的左边文本
$' 匹配到的子串的右边文本
$$ 美元符号

比如将 2019/12/29 替换成 2019-12-28,可以这么做,这里解释了$1,$2含义:

var str = '2019/12/29';
console.log(str.replace(/(\d{4})\/(\d{2})\/(\d{2})/g, '$1-$2-$3')); //2019-12-29

console.log(RegExp.$1); //2019
console.log(RegExp.$2); //12
console.log(RegExp.$3); //29

我们来通过一个例子来解释$&,如下:

var result = "a,b,c".replace(/\w/g, "$&$&");
console.log(result);//aa,bb,cc

很多人看到这里可能就不理解了,这是怎么替换的?记住一点,replace具有遍历特性,前面正则能匹配到几次,后面的替换就会执行几次。

第一次匹配到了a,所以此时的 $&表示 a,执行替换,a被替换成了 aa。

第二次匹配到了b,此时的$&表示 b,又执行替换,此时b又变成了bb,以此类推,经历过三次替换于是变成了aa,bb,cc。

我们通过一个例子来解释 $` 和 $':

var result = "2+3=5".replace(/=/, "$`----$'");
console.log(result); //2+32+3----55

整个匹配下来只有一个 = ,所以$`表示 = 左边的内容,也就是2+3,对应的 $'表示 = 右边的内容,也就是5。别忘了replace是将匹配到的内容替换掉,所以 = 被替换成了 2+3----5。

说到这肯定有同学问了,要死我有多个 = 符号你咋替换,别忘了replace是会遍历的,它会循环起来一步步替换,看个例子:

var result = "2=3=5".replace(/=/g, "$`$'");
console.log(result); //223=532=355

由于加了修饰符g,所以这里会匹配量词,先说第一次,=左边是2,右边是3=5,组合起来就是 23=5,替换到=上去之后就是223=53=5。

匹配到了第二个 = ,注意,此时 = 左右不是以修改的字符作为标准,而是继续以修改前的原字符作为切割标准,所以第二个 = 左边是2=3,右边是5,组合起来就是2=35,替换到 223=53=5 第二个 = 上,于是就变成了 223=532=355。

不难理解,但是懒得理解....实际开发中使用$1此类字符串较多,其它字符就随缘了。但其实说到这里,我们还是强调一点,replace会遍历替换。

replace方法第二个参数还可以是一个回调函数,这里就可以验证我们说的replace会遍历的说法:

"1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (match, $1, $2, index, input) {
    console.log([match, $1, $2, index, input]);
});
// ["1234", "1", "4", 0, "1234 2345 3456"]
// ["2345", "2", "5", 5, "1234 2345 3456"]
// ["3456", "3", "6", 10, "1234 2345 3456"]

8.正则构造函数

与一般对象创建可以使用对象字面量,构造函数创建对象一样,正则也能使用构造函数创建。请记住一点,能使用字面量的情况一定使用字面量。有一种特殊情况必须使用构造函数创建,那就是正则内容是一个变量,比如:

function regexp(param) {
    var regex = new RegExp(param, 'g');
    console.log(regex.test('听风是风'));//true
    console.log(regex.test('时间跳跃 听风是风'));//true
    console.log(regex.test('时间跳跃'));//false
};
regexp('听风是风');

当然就算不是变量情况,我们也能用构造函数创建固定字符的正则,但原本有\的字符前你得多加一个\,所以你会发现这样的正则特别难读,像这样:

var regex = new RegExp('\\d+\\.\\d+', 'g');
console.log(regex.test('3.14')); //true
console.log(regex.test('.3.14')); //false

//等同于
var regex = /\d+\.\d+/g;

这个正则其实只是用来匹配浮点数,但\d和为了匹配小数点使用了转义符的\d前面都有\,所以统统再得加一个\。所以说嘛,能不用构造函数记得一定不要用。

9.正则source属性

使用构造函数创建正则的另一个问题就是,你不知道这是不是你想要的正则,对于这一点,我们可以通过source属性查看:

var regex = new RegExp('\\d+\\.\\d+', 'g');
console.log(regex.source) //\d+\.\d+

10.构造函数属性

在前面聊$1,$&时,我想大家一定有个问题,难道我每次都要根据替换结果来反推这些字符的意思吗?有没有什么办法直接查看呢,其实是有的:

静态属性 描述 简写方式
 RegExp.input  最近一次目标字符串  RegExp["$_"]
 RegExp.lastMatch  最近一次匹配的文本  RegExp["$&"]
 RegExp.lastParen  最近一次捕获的文本  RegExp["$+"]
 RegExp.leftContext  目标字符串中lastMatch之前的文本  RegExp["$`"]
 RegExp.rightContext  目标字符串中lastMatch之后的文本  RegExp["$'"]

通过静态属性或简写方式,我们可以直接查看这些字符到底匹配到了什么东西:

var regex = /\w(=)/g;
var string = 'a=b=c';
string.match(regex);
console.log(RegExp.input);
console.log(RegExp["$_"]);
//a=b=c
console.log(RegExp.lastMatch);
console.log(RegExp["$&"]);
// b=
console.log(RegExp.lastParen);
console.log(RegExp["$+"]);
// =
console.log(RegExp.leftContext);
console.log(RegExp["$`"]);
// a=
console.log(RegExp.rightContext);
console.log(RegExp["$'"]);
// c

 肆 ❀ 总

我读完了这本正则表示迷你书,很开心。然后现在断网了,博客无法提交保存,只能用USB链接电脑苟延残喘写个结尾提交了。

不管怎么说,我一路学过来也顺利学完,还是非常推荐大学花点时间好好学习正则,因为学会了之后在工作中每解决一个正则问题,那种前所未有的快感真是爽到爆炸!!!

好啦,半个月的学习顺利结束,也希望你能爱上正则,一起努力吧,有缘看到此文但陌生的你。

posted on 2019-12-28 23:35  听风是风  阅读(525)  评论(2编辑  收藏  举报