正则详解及实战(JavaScript)

2,040 阅读13分钟

一、正则初体验


在软件开发中,不管是Java、C#、JS、OC....基本上都会接触到正则,不过大多数人都对正则并不是很了解,正常情况下都是直接从网上扒一段(我之前也一直都是这么干的,哈哈)。但是做开发时间比较久之后,再遇到正则就特别想深入的去了解,而且听不少朋友说,最近面试的时候遇到不少关于正则问题......

1.1、什么是正则

简单来说,正则就可以理解为一个规则,用来处理字符串的一个规则(正则就是用来处理字符串的),这里所说处理一般是包含匹配捕获

  • 匹配:判断一个字符串是否符合指定的规则,使用test方法:reg.test(str)

    var reg = /\d/; // 包含一个0~9之间的数字
    console.log(reg.test('天')); // false
    console.log(reg.test('1')); // true
    console.log(reg.test('现在是2017')); // true,只要包含了数字就返回true
  • 捕获:把字符串中符合指定的正则规则的内容捕获到,使用exec方法:reg.exec(str)

    var reg = /\d/;
    console.log(reg.exec('天')); // null
    console.log(reg.exec('1')); // ["1", index: 0, input: "1"]
    console.log(reg.exec('现在是2017')); // ["2", index: 3, input: "现在是2017"]

1.2、如何创建正则

  • 字面量方式

    var reg = /\d/;
  • 实例方式

    var reg = new RegExp('/\d/');

数组等对象的字面量方式创建和实例方式创建出来的数组差别并不是很大,但是正则的这两种方式创建出来是有挺大的区别的。具体是哪些区别,现在说还不合适,先卖个关子,接下来的内容中讲解。

二、正则的组成


从上面的内容中已经了解到,每一个正则表达式是包含在//中的,正则表达式就是匹配规则,正则的组成就是元字符修饰符

2.1、元字符

  • 具有特殊意义的元字符

    • \:转义字符,转义后面字符所代表的含义
    • ^:以某一个元字符开始
    • $:以某一个元字符结束
    • \n:匹配一个换行符
    • . :除了\n以外的任意字符
    var reg = /^0.2$/; // 以0开头,以2结尾,中间可以是除了\n的任意字符
    console.log(reg.test('0.2')); // true
    console.log(reg.test('0-2')); // true
    
    reg = /^0\.2$/; // 将"."转义
    console.log(reg.test('0.2')); // true
    console.log(reg.test('0-2')); // false
  • 代表出现次数的量词元字符

    • *:出现0到多次
    • +:出现1到多次
    • ?:出现0次或者1次
    • {n}:出现n次
    • {n,m}:出现n到m次
    var reg = /^\d+$/;
    console.log(reg.test('2015')); // true

2.2、修饰符

  • x|y:x或y中的一个
  • [xyz]:x或y或z中的一个
  • [^xyz]:除了xyz以外的任意一个字符
  • [a-z]:a-z之间的任何一个字符
  • [^a-z]:除了a-z之间的任何一个字符
  • \d:一个0~9之间的数字
  • \D:除了0~9之间的数字以外的任何字符
  • \b:一个边界符
  • \w:数字、字母、下划线中的任意一个字符
  • \s:匹配一个空白字符、空格
  • ():分组,把一个大正则本身划分成几个小的正则,例如:var reg = /^(\d+)zhufeng(\d+)$/;

三、元字符的应用


在做元字符的应用前,有必要先了解下中括号和分组的使用,然后能更好的做应用。

3.1、[]的规律

  • 在中括号中出现的所有的字符都是代表本身的意思的字符(没有特殊含义)

    var reg = /^[.]$/;
    console.log(reg.test('1')); // false
    console.log(reg.test('.')); // true
    
    reg = /^[\w-]$/; // 数字、字母、下划线、- 中的一个
    console.log(reg.test('-')); // true
  • 中括号不识别两位数

    var reg = /^[12]$/; // --> 1或者2中的一个(符合[xyz])
    var reg = /^[12-68]$/; // --> 1、2-6中的一个、8  三个中的一个

3.2、()的作用

分组的作用有很多,现在先讲其中的一个:改变x|y的默认的优先级,还有的在后面的内容会详细介绍。

var reg = /^18|19$/; // 18、19、181、189、119、819、1819这些都符合
var reg = /^(18|19)$/; // 只能18或者19

3.3、应用一:有效数字的正则

有效数字可以是正数、负数、零、小数,所以其特点为:

  • "."可以出现也可以不出现,一旦出现,后面必须跟着一位或多为数字;

  • 最开始可能有“+/-”,也可以没有;

  • 整数部分,一位数的情况可以是0-9中的一个,多位数的情况下不能以0开头;

var reg = /^[+-]?(\d|([1-9]\d+))(\.\d+)?$/;

3.4、应用二:年龄介于18~65之间

年龄介于18~65之间的数字可以是18-19、20-59、60-65。

var reg = /^1[8-9]|[2-5]\d|6[0-5]$/;

3.5、应用三:简单的邮箱验证

var reg = /^[\w.-]+@[0-9a-zA-Z]+(\.[a-zA-Z]{2,4}){1,2}$/;

四、两种方式创建正则的区别


在开始的时候,我们介绍了创建正则有两种方式:字面量方式、对象方式。在字面量方式中,"//"之间包起来的所有的内容都是元字符,有的具有特殊的意义,大部分都是代表本身含义的普通元字符。

现在有这么一个场景,就是正则中的某一段内容是不固定的,那么我们用字面量的方式可能会这么写:

var name = 'iceman';
var reg = /^\d+"+name+"\d+$/;
console.log(reg.test('2015iceman2016')); // false
console.log(reg.test('2015"""nameeee"2016')); // true

我们的想法很美好的,假设name是动态设置的,内容为“iceman”,那么'2015iceman2016'是肯定能适配reg的啊,但结果却是false。

第二条输出中却是true!是不是很崩溃呢? 不过再认真看一下,我们在第二条匹配的字符串中写了三个引号,name的后面再加了三个e。看到这里,是不是发现了什么呢:


  • 正则.png

没错,在字面量方式创建的正则中,引号和单独出现的加号都被当成了普通的元字符。

对于上面的这个需求,我们只能使用实例创建正则的方式:

var name = 'iceman';
var reg = new RegExp("^\\d+" + name + "\\d+$", "g");
console.log(reg.test('2015iceman2016')); // true

所以总结字面量方式和实例方式创建正则的区别:

  • 字面量方式中出现的一切都是元字符,不能进行变量值的拼接,而实例创建的方式可以;

  • 字面量方式中直接写\d可以,而在实例中需要把它转义 \\d

五、正则的捕获及其贪婪性和懒惰性


在上面有介绍到正则的捕获使用exec方法。在每一次捕获的时候都是先进行默认的匹配,如果没有匹配成功,则捕获的结果是null。只有有匹配的内容,才能捕获到。

5.1、懒惰性

定义正则及字符串:

var reg = /\d+/;
var str = 'iceman2016learn2017';

reg默认有一个lastIndex字段,该字段是正则每一次捕获时,在字符串中开始查找的位置,默认的值是0。

现在先进行第一次捕获:

console.log(reg.lastIndex); // 0,第一次捕获的时候,从字符串索引0处开始查找
var res = reg.exec(str);
console.log(res); // ["2016", index: 6, input: "iceman2016learn2017"]

从代码的输出可知,正则捕获的内容格式:捕获到的内容是一个数组

  • 数组的第一项是当前大正则捕获的内容;
  • 有一项是index:捕获内容在字符串中开始的索引位置;
  • 有一项是input:捕获的原始字符串;

现在进行第二次捕获:

console.log(reg.lastIndex); // 0  说明第二次捕获的时候,也要从字符串索引0处开始查找
// 第二次通过exec捕获的内容还是第一个"2016"
res = reg.exec(str);
console.log(res); //["2016", index: 6, input: "iceman2016learn2017"]

由上面的两次捕获可知,每次的捕获都是从字符串的索引0处开始查找的,这就是正则的懒惰型

正则懒惰型的特点:每一次执行exec只捕获第一个匹配的内容,在不进行任何处理的情况下,在执行多次捕获的时候,捕获的还是第一个匹配的内容。

很明显正则的懒惰性是我们所要解决的问题,那么该如何解决懒惰性呢? 答案就是在正则的末尾加一个修饰“g”(全局匹配),类似g这样的修饰符还有两个:i、m,这三者的作用是:

  • global(g):全局匹配

  • ignoreCase(i):忽略大小写

  • multiline(m):多行匹配

var reg = /\d+/g;
var str = 'iceman2016learn2017';
console.log(reg.lastIndex); // 0
console.log(reg.exec(str)); // ["2016", index: 6, input: "iceman2016learn2017"]

console.log(reg.lastIndex); // 10
console.log(reg.exec(str)); // ["2017", index: 15, input: "iceman2016learn2017"]

console.log(reg.lastIndex); // 19
console.log(reg.exec(str)); // null

在加了修饰符g之后,就解决了懒惰型,达到了我们想要的效果,所以全局修饰符g的原理是:正则每一次捕获结束后,lastIndex的值都变成了最新的值,下一次捕获从最新的位置开始查找,这样就可以把所有需要捕获的内容都获取到了。

自己编写程序获取正则捕获的所有内容(正则一定要加g哦!!!)

var reg = /\d+/g;
var str = 'iceman2016learn2017';
var ary = [];
var res = reg.exec(str);
while (res) {
    ary.push(res[0]);
    res = reg.exec(str);
}
console.log(ary);

5.2、贪婪性

var reg = /\d+/g; // 出现一到多个0~9之间的数字
var str = 'iceman2016learn2017javascript2018';
console.log(reg.exec(str)); // ["2016", index: 6, input: "iceman2016learn2017javascript2018"]

看到这段代码的时候不知道您有没有一些疑惑,正则的内容是/\d+/,是匹配1到多个数字,2015是符合正则的,那么2也是符合正则的啊,为什么默认就捕获了2015呢? 这就是正则的贪婪性

如何解决正则的贪婪性:在量词元字符后面添加一个"?"即可。

var reg = /\d+?/g; // 出现一到多个0~9之间的数字
var str = 'iceman2016learn2017javascript2018';
console.log(reg.exec(str)); // ["2", index: 6, input: "iceman2016learn2017javascript2018"]
var ary = [] , res = reg.exec(str);
while (res) {
    ary.push(res[0]);
    res = reg.exec(str)
}
console.log(ary); // ["0", "1", "6", "2", "0", "1", "7", "2", "0", "1", "8"]

"?"在正则中的作用

  • 放在一个普通的元字符后面,代表出现0~1次;

  • 放在一个量词的元字符后面,取消捕获时候的贪婪性;

5.3、字符串中的match方法

match方法的作用是,把所有和正则匹配的字符都获取到。

var reg = /\d+?/g;
var str = 'zhufeng2015peixun2016dasgdas2017';
var ary = str.match(reg);
console.log(ary); // ["2", "0", "1", "5", "2", "0", "1", "6", "2", "0", "1", "7"]

注意:虽然在当前的情况下,match比exec更加的简洁一些,但是match存在一些自己处理不了的问题:在分组捕获的情况下,match只能捕获到大正则,而对于小正则捕获的内容是无法获取的。

六、分组捕获


6.1、正则分组

正则分组的两个作用:

  • 改变优先级(在“三、元字符的应用”中已经有介绍到)

  • 分组引用
    \2代表和第二个分组出现一模一样(和对应的分组中的内容和值都要一样)的内容,\1代表和第一个分组出现一模一样的内容;

    var reg = /^(\w)(\w)\1\2$/;
    console.log(reg.test("icic")); // true
    console.log(reg.test("r0g_")); // false

6.2、分组捕获

正则在捕获的时候,不仅仅把大正则匹配的内容捕获到,而且还可以把小分组匹配的内容捕获到。

身份证中的数字都有意义的,比如开头的两位代表省,中间的四位代表...所以对于一个身份中,有必要对其中的数字按照其意义进行分组捕获。

var reg = /^(\d{2})(\d{4})(\d{4})(\d{2})(\d{2})(?:\d{2})(\d)(?:\d|X)$/;
var str = "350324202904190216";
console.log(reg.exec(str));

注意:(?:) 在分组中?:的意思是只匹配不捕获

输出的内容为:["350324200904190216", "35", "0324", "2009", "04", "19", "1", index: 0, input: "350324200904190216"]

其中:

  • 350324200904190216 :大正则匹配的内容
  • 35 :第一个分组捕获的内容
  • 0324 :第二个分组捕获的内容
  • ……

在这里使用match方法的话,和exec获取的内容一样:

console.log(str.match(reg));

再看一个例子:

var reg = /ice(\d+)/g;
var str = 'ice1234ice3456ice5678'
// 用exec执行三次,每一次不仅仅把大正则匹配的获取到,而且还可以获取第一个分组匹配的内容
console.log(reg.exec(str)); // ["ice1234", "1234", index: 0, input: "ice1234ice3456ice5678"]
console.log(reg.exec(str)); // ["ice3456", "3456", index: 7, input: "ice1234ice3456ice5678"]
console.log(reg.exec(str)); // ["ice5678", "5678", index: 14, input: "ice1234ice3456ice5678"]

// 而match只能捕获大正则
console.log(str.match(reg)); // ["ice1234", "ice3456", "ice5678"]

此时match是只能捕获大正则的内容,所以match能做到的exec都能做到,match做不到的exec也能做到。

总结:只捕获一次就好的,那么用exec和match都可以,像本例中要连续捕获三次的,用match就捕获不到小正则了。

七、replace基础


var str = 'iceman2016iceman2017';

在上面定义的字符串中,现在需要将iceman替换成shoushou,我们知道字符串提供了一个replace方法,那么我们用replace来做一次:

str = str.replace('iceman','shoushou');
console.log(str); // shoushou2016iceman2017

从打印的结果可知,并没有实现我们想要的效果,只替换了第一个iceman字符串,看MDN中对于replace方法的定义:

str.replace(regexp|substr, newSubStr|function)
replace() 方法返回一个由替换值替换一些或所有匹配的模式后的新字符串。模式可以是一个字符串或者一个正则表达式, 替换值可以是一个字符串或者一个每次匹配都要调用的函数。

由replace的定义可知,匹配模式可以是一个正则的,所以我们用正则试一次:

str = str.replace(/iceman/g, 'shoushou');
console.log(str); // shoushou2016shoushou2017

在使用正则的时候已经实现了需求,并且注意要使用修饰符g让正则全局捕获。

replace第一项的值是正则的情况下的实现原理:首先和exec捕获一样,把所有和正则匹配的内容都捕获到,然后把捕获的内容替换成我们需要替换的新内容, 在这里就是按照/iceman/g 把str中所有可以匹配的都捕获到,然后替换成iceman。

再看replace函数的定义,第二个参数可以是一个函数:

var str = 'iceman2016iceman2017';
str = str.replace(/iceman/g, function () {
    // 第一次执行匿名函数输出arguments的结果:["iceman", 0, "iceman2016iceman2017"]
    // 第二次执行匿名函数输出arguments的结果:["iceman", 10, "iceman2016iceman2017"]
    console.log(arguments);
    return 'shoushou';
});
console.log(str); // shoushou2016shoushou2017

从打印结果可知:

  • 先按照正则指定的规则,到字符串中把正则匹配的内容捕获到,然后在每一次捕获到之后,都把捕获的内容替换成新的内容;

  • 匿名函数执行多少次,取决于正则能在字符串中捕获多少次,在这里正则捕获两次,所以匿名函数也执行两次;

  • 每一次执行匿名函数,里面传递的参数值arguments和自己通过exec捕获到的结果是一样的(每一次执行匿名函数,和单独执行exec捕获的内容一致);

  • 小分组捕获的内容,在这里同样可以获取到(所以说,replace和exec原理是一模一样的,比match要强大)

  • return的结果是什么,就相当于把当前这一次大正则捕获的内容替换成返回的内容。如果不写return,默认使用undefined来进行替换,如果不想实现替换的话,我们可以把捕获的内容再返回回去 return arguments[0]

var str = 'iceman2015iceman2016';
var reg = /iceman(\d+)/g;
str = str.replace(reg, function () {
    // 第一次捕获arguments的值:["iceman2015", "2015", 0, "iceman2015iceman2016"]
    // 第二次捕获arguments的值:["iceman2016", "2016", 10, "iceman2015iceman2016"]
     console.log(arguments);
});

从打印中可知,当有分组的时候,arguments的第二个参数开始分组的内容,所以可以用arguments[1]这样的方式来获取分组的内容。

八、replace实战


从上面的介绍中已经知道,正则的捕获有三种方式:正则的exec方法、字符串的match方法、字符串的replace方法。

其中replace是将原有的字符串替换成我们想要的新的字符串,在不适用正则的情况下,执行一次replace只能替换字符串中的一个,而使用正则的话,可以一次批量的把所有的正则匹配的内容都替换掉。

8.1、实战一:小写数字替换成大写数字

var str = '今年是2017年'; //
var ary = ['零', '壹', '贰', '叁', '肆', '伍', '陆','柒', '捌', '玖', '拾'];
// 实现替换的话,需要捕获到数字,并且把数字当作ary的索引获取对应的汉字进行替换
str = str.replace(/\d/g, function () {
    /*
     * 第一次执行:大正则捕获的是2,返回的是ary[2] --> '贰'
     * 第二次执行:大正则捕获的是0,返回的是ary[0] --> '零'
     * .....
     */
    return ary[arguments[0]];
});
console.log(str);

8.2、实战二:获取一个字符串中出现次数最多的字符,并且获取出现的次数

var str = 'zhongguofujianxiamensimingshoushou';
// 1)获取每一个字符出现的次数
var obj = {};
str.replace(/[a-z]/gi, function () {
    var val = arguments[0];
    obj[val] >= 1 ? obj[val] += 1: obj[val] = 1;
});

// 2)获取最多出现的次数
var maxNum = 0;
for (var key in obj) {
    obj[key] > maxNum ? maxNum = obj[key] : null;
}

// 3)把所有符合出现maxNum次数的都获取到
var ary = [];
for (var key in obj) {
    obj[key] === maxNum ? ary.push(key) : null;
}
console.log('整个字符串中出现次数最多的字符是:' + ary.toString() + '出现了' + maxNum + '次');

8.3、实战三:模板引擎实现的初步原理

var str = 'my name is {0}, my age is {1}, i come from {2}, i love {3} ~~~';
var ary = ['iceman', '26', 'China', 'javascript']
str = str.replace(/{(\d+)}/g , function () {
     return ary[arguments[1]];
});
console.log(str);