正则表达式是准备面试时经常被忽略的内容,所以这个主题的题目在面试中成功率很低。熟悉正则表达式最好的方法不应该是停留在理论上,应该是多多练习题目,常练常新。所以这一节会让大家在练习正则表达式的题目的同时,多多通过题目来讲解正则表达式的技巧。
1. 在 JavaScript 的字符串与正则表达式操作中,test、exec、 match 这三个方法的用法是什么,正则表达式中的括号在这三个方法中的作用分别是什么?
答案:
RegExp.prototype.test()
test
是JavaScript中正则表达式对象的一个方法,用来检测正则表达式对象与传入的字符串是否匹配,若匹配返回true
,若不匹配返回false
,其使用方法为:
regexObj.test(str)
如:
/Jack/.test('ack') // false
由于test
方法的返回值只是一个boolean值,所以括号在test方法中唯一的作用就是分组,如:
/123{2}/.test('123123') // false
/(123){2}/.test('123123') // true
String.prototype.match()
match
方法是字符串的方法,传入参数为正则表达式,返回字符串匹配正则表达式的结果。括号在match
方法中有两个作用:
-
分组
-
捕获。捕获的意思是将用户指定的匹配到的子字符串暂存并返回给用户。
'123123'.match(/123{2}/) // null '123123'.match(/(123){2}/) // ["123123", "123", index: 0, input: "123123", groups: undefined]
当传入的正则表达式没有使用g标志时,其返回一个数组,数组第一个值为第一个完整匹配,后续的值分别为括号捕获的所有值,且此数组含有以下三个属性:
groups
,命名捕获组index
,匹配结果的开始下标input
,传入的原始字符串
如:
const result1 = '123123'.match(/123{2}/) // null
const result2 = '123123'.match(/(123){2}/) // ["123123", "123", index: 0, input: "123123", groups: undefined]
console.log(result2.index) // 0
console.log(result2.input) // 123123
console.log(result2.groups) // undefined
对比这两个匹配返回的结果,可以证明括号起到了分组的作用。再看第二句,返回数组的第一个值为正则匹配到的第一个子字符串,第二个值为括号捕获的值。
虽然第二句用括号引用了捕获组,但是result2.groups
的值依然为undefined
,这是因为第二句中的捕获组为匿名捕获组,而result2.groups
只返回命名捕获组的值,命名捕获组的用法是:
(?<name>...)
例如:
const result = 'a123a123'.match(/a(?<first>\d)(?<second>\d)/) // ["a12", "1", "2", index: 0, input: "a123a123", groups: {…}]
console.log(result.index) // 0
console.log(result.input) // a123a123
console.log(result.groups) // {first: "1", second: "2"}
在match
方法中,当传入的正则表达式有 g 标志时,将返回所有与正则表达式匹配的结果,忽略捕获,如:
const result = 'a123a123'.match(/a(?<first>\d)(?<second>\d)/g) // ["a12", "a12"]
RegExp.prototype.exec
exec
方法是正则表达式的方法,传入参数为字符串,返回字符串匹配正则表达式的结果。当正则表达式没有g标志时,其返回值与String.prototype.match()
没有g标志时返回的结果一样:
'123123'.match(/(123){2}/) // ["123123", "123", index: 0, input: "123123", groups: undefined]
/(123){2}/.exec('123123') // ["123123", "123", index: 0, input: "123123", groups: undefined]
当正则表达式有 g 标志时,可以多次执行 exec 方法来查找同一个字符串中的成功匹配。如:
var html = "123123";
var tag = /(12)3/g
var match, arr = []
do {
if (match) arr.push(match)
match = tag.exec(html)
} while (match)
console.log(arr[0]) // ["123", "12", index: 0, input: "123123", groups: undefined]
console.log(arr[1]) // ["123", "12", index: 3, input: "123123", groups: undefined]
console.log(arr[2]) // undefined
exec
方法中的括号用法与match
方法相同。
解读:
很多同学对test
方法比较熟悉,但是对match
和exec
方法不熟悉,我想主要原因还是因为不太熟悉这三个方法的应用场景,在这里我们总结一下:
- 当你只是想检测字符串和正则表达式是否匹配时,使用
test
方法 - 当你不仅想检测两者是否匹配,还想知道是哪些子字符串匹配,或者想要在非global模式下捕获子字符串时,用
match
方法 - 当你既想在global模式下匹配字符串,又想捕获子字符串时,就是用
exec
方法
在这道题中,我们还总结了括号在正则表达式中最重要的两个用法:分组和捕获。分组是很符合直觉很好理解的一种用法,但是捕获要稍微难一些。
2. 请写出一个正则表达式,支持匹配如下三种日期格式:
2016-06-12
2016/06/12
2016.06.12
答案:
/\d{4}(-|\/|\.)\d{2}\1\d{2}/
解读:
很多同学一开始可能会这样回答:
/\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/
这个正则表达式匹配题目中的三个日期确实返回true
,但是:
/\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/.test("2016-06/12") // true
这样也会返回true
,而题目中明显要求是要有统一的分隔字符,所以不符合题目要求。既然要有统一的分隔字符,就要求能有一种机制来捕获第一个分隔符,然后第二个分隔符与其保持一致。此时就需要反向引用了,注意答案中将第一个分隔符用括号括住来进行捕获,然后答案中d{2}
和d{2}
之间的\1
,代表之前匹配到的第一个字符,这样就实现了两个字符统一的效果。
3. 这个表达式的结果是什么,为什么?
/\d\B\D\w{2}\W{2}\s\S\b/.test('1a_a(& s')
答案:
打印出:true
解读:
这道题目的目的是考察面试者对于正则表达式中元字符和限定符的理解。
常用元字符定义如下:
\d
代表整数\B
代表非边界\D
代表非整数\w
是word的缩写,等价于[A-Za-z0-9_]
,{2}
代表前面的元字符出现两次\s
代表空白字符\S
代表非空白字符\b
代表边界
常用限定符定义如下:
*
:出现零次或多次?
:出现一次或零次+
:出现一次或多次{n}
:出现n次{n,}
:至少出现n次{n,m}
:出现n次至m次
另外一个很容易被忽略的点是,要记清转义字符倾斜的方向,我发现当单独写转义字符时,很多人能记住方向,但是和正则表达式的标识符/
混在一起时,一些面试者就会突然记不清了。
4. 请写一段代码,计算一个 html 文档标签个数(开始和结束标签算两个),要求使用到正则表达式匹配。例如
const html = `<div class='test'><b>Hello</b> <i>world!</i></div>`
应该打印出:6
答案:
const tag = /<\/?(\w+)([^>]*)>/g
let match, arr = []
do {
if (match) arr.push(match)
match = tag.exec(html)
} while (match)
console.log(arr.length)
解读:
这道题考察了对exec
方法的运用。这个正则的前面一部分比较好理解,需要注意的是后面那部分:([^>]*?)
。[]
代表集合,^
放在方括号中表示非,也就是说[^>]
代表所有非>
的字符,*
代表任意数量,意即匹配下一个>
前面的所有字符,最后用一个>
结尾。
有的同学可能会这样写:
/<\/?(\w+)([^>]*)>/g
\w
是word的缩写,等价于[A-Za-z0-9_]
,所以会出现如下的结果:
/<\/?(\w+)>/g.test("<div class='test'>") // false
这是因为字符串中的div
后面跟的空格与=
、'
等字符与\w
不匹配。所以这样是不对的。
也有的同学会这样写:
const tag = /<\/?([^>]*)>/g
这样会出现如下的结果:
/<\/?([^>]*)>/g.test('<**>') // true
所以这样也是不可以的。
5. 写一个函数,将驼峰字符串转换成-分割字符串。
答案:
function conversion(str) {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}
解读:
这道题考察了正则表达式与String.prototype.replace
结合处理字符串的能力。$1
类似于前面提到的\1
,代表了前面捕获到的值。
6. 请问下面代码会打印出什么?
const reg1 = /n?a+m*e{2}n{2,4}a{2,}/
const reg2 = /n??a+?m*?e{2}?n{2,4}?a{2,}?/
const string = 'aammmmeennnaaaaa'
const r1 = string.match(reg1)[0]
const r2 = string.match(reg2)[0]
console.log(r1)
console.log(r2)
答案:
aammmmeennnaaaaa
aammmmeennnaa
解读:
这道题考察了正则表达式中的贪心模式与非贪心模式。相信很多同学都知道正则表达式里的?
是众多量词中的一个,代表匹配一次或多次,其他的量词还有:
-
*
:匹配零次或多次 -
+
:匹配一次或多次 -
{n}
:匹配n次 -
{n,}
:至少n次 -
{n,m}
:匹配n到m次
刚才提到的所有的量词都出现在了第一个正则表达式中,正则表达式匹配时,默认采用贪心模式,意即尽量多地匹配字符串,比如在第一个正则表达式匹配时,正则表达式以a{2,}
结尾,由于默认为贪心模式,所以正则会尽量多地匹配,所以会匹配到字符串结尾的所有a
字符。
那么如何进行非贪婪模式的匹配呢?只需要在量词后面加上一个?
字符就行,也就是说?
跟在字符后面就是量词,跟在量词后面就是开启非贪心模式。如第二个正则表达式,以a{2,}?
为结尾,所以会尽量少地匹配字符串,所以匹配出的字符串结尾只有2个a
。
有的同学会回答 r2 的值是:
aeennaa
回答出这个答案的同学是明白第二个正则表达式是非贪心模式的,但是正则表达式是这样匹配的:
所以aeennaa
这个答案是不对的。
7. 如何给数字加上千分位分隔符?
答案:
-
使用
toLocaleString
方法const num = 1234567
num.toLocaleString() // 1,234,567 -
使用正则表达式
‘1234567’.replace(/(?=(\B\d{3})+$)/g, ‘,’) // 1,234,567
解读:
这道题目当然着重考察面试者对正则表达式的掌握,但是如果能在答出第二种方法的基础上还能答出第一种方法,这个答案就完美了。
对于这个正则表达式,其主要考察了正向断言的使用。正向断言的意思是:指定一个子字符串,匹配这个字字符串前面符合规则的字符串。这样讲感觉有点绕,可以看一下这个例子:
const reg=/\d+(\.\d+)?(?=元)/g;
const str="我的口袋里还有10元,你的口袋里还有12.4元";
str.match(reg) // ["10", "12.4"]
第一行的正则表达式的意思是:我要匹配的字符串满足两个条件:
- 符合表达式
\d+(\.\d+)?
- 后面跟着
=元
可以看到,?=
断言了匹配目标后面要跟着的子字符串,而这个子字符串 (元
)是不会出现在匹配结果中的。
再来练习一次:
'000111222'.match(/(?=(\d{3})+$)/g) // ["", "", ""]
这个表达式的含义是:匹配后面跟着3倍数的数字的空字符串
这正是我们在这道题目中想要的,匹配后面跟着3倍数的数字的空字符串然后替换成,
,但是如果直接使用这个正则会出现这种情况:
'123456789'.replace(/(?=(\d{3})+$)/g, ',') // ,123,456,789
可以看到当数字的长度为3的倍数时,字符串的开头被加上了一个,
, 所以我们要排除掉字符串的开头,这就要用到\B
元字符:
'1234567'.replace(/(?=(\B\d{3})+$)/g, ',') // 1,234,567
总结:
正则表达式的内容其实并不难,主要是平时写代码时练习的机会不多,导致这次总结的知识下次就忘掉了。建议大家也可以自己总结类似于第一题这种题目,隔一段时间拿出来做一做,常用的元字符、限定符、量词、贪婪模式、正向断言等就能记住了。
正则表达式的另一个难点就是同样一个符号,却能代表不同的含义。
比如^
符号,当它出现在正则表达式开头的时候,标识匹配字符串开始的位置,比如:
/23/.test('123') // 没有限定开始位置,匹配到了23,所以返回true
/^23/.test('123') // 限定字符串必须以2开始,所以返回false
但是当其出现在字符组开头时,却又表示非 (排除),例如:
/[123]/.test(1) // 这个正则表达式的含义是匹配1、2、3中任意一个,所以返回true
/[^123]/.test(1) // 这个正则表达式的含义是匹配所有除了1、2、3的任意字符,所以返回false
还有?
既可以表示量词,还可以表示非贪婪模式;这部分内容就需要大家细心总结,多多练习了。再给大家推荐一个网站:https://regex101.com/,大家可以多多在里面输入不同的正则表达式,而它会根据用户输入的表达式来进行描述。