面试官:我来考一下正则吧

758 阅读13分钟

不知道大家是不是跟我一样,对正则有一种莫名奇妙的恐惧,日常开发遇到要正则匹配,百度google一把梭哈,但是面试的时候面试官问这个问题的时候,心里就开始悔恨为什么当初不好好学。这也是大家在准备面试的过程中最容易忽略的一个知识点,所以面试成功率很低,今天,就让我们来克服内心的恐惧,看看笔试面试中,面试官能把正则问出什么花来!

RegExp对象

RegExp是js内置的对象,用来进行正则匹配。 有两种方法实例化RegExp对象,分别是:

字面量:

const reg = /hello/
const a = 'helloworld'.replace(reg,'HELLO')  // HELLOworld 

构造函数

const reg = new RegExp('hello')
const a = 'helloworld'.replace(reg,'HELLO')   //HELLOworld

使用replace方法,如果被第二个参数字符串替换掉,那说明匹配上了,而且前端日常开发很多时候就是使用replace对正则匹配到的字符进行操作的,正则多跟着例子打一打,就会变得越来越熟练啦!

修饰符

i 执行对大小写不敏感的匹配。

g 执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。

m 执行多行匹配。

例如:

const reg = /hello/g
const a = 'helloworldhello'.replace(reg,'HELLO')  // HELLOworldHELLO 

const reg1 = /hello/i
const b = 'hellOworld'.replace(reg1,'HELLO')  // HELLOworld 

元字符

正则表达式由两种基本字符类型组成

  • 原义文本字符 a,b,c...(就代表字母本身的含义,例如上面举例的hello,指的就是要匹配字符串中的hello,没有其他特别含义)

  • 元字符 含有特殊含义的字符

最简单的正则表达式只要字母数字就可以完成了,比如上面的那个例子,但是我们知道,往往没有那么简单。

看看下面这道题:

const reg = /\d\B\D\w{2}\W{2}\s\S\b/
const a = '1a_a(& s'.replace(reg,'匹配成功')

你能说出上面那条表达式的结果吗?? 说不上来?没关系,让我们来好好说道说道。

单字符

除了字母和数字,我们想要匹配特殊字符时,就得请出我们第一个元字符 \ , 它是转义字符字符,顾名思义,就是让其后续的字符失去其本来的含义。举个例子:

我想匹配*这个符号,由于*这个符号本身是个特殊字符,所以我要利用转义元字符\来让它失去其本来的含义:

所以,使用/*/就是单纯的要匹配 * 这个字符

const reg = /\*/g 
const a = '*world*'.replace(reg,'HELLO')  //HELLOworldHELLO

如果本来这个字符不是特殊字符,使用转义符号就会让它拥有特殊的含义。我们常常需要匹配一些特殊字符,比如空格,制表符,回车,换行等, 而这些就需要我们使用转义字符来匹配。这个就需要我们去记忆啦!记住转义符号的方式是\,不要记反啦!

字符类和范围类

那如果我想匹配123其中的一个字符都可以,那么我就需要使用中括号[ ],在正则表达式里,集合的定义方式是使用中括号[]。如/[123]/这个正则就能同时匹配1,2,3三个字符。

const reg = /[123]/
const a = '1'.replace(reg,'HELLO')  //HELLO
const b = '2'.replace(reg,'HELLO')  //HELLO

那如果我想匹配所有的数字怎么办呢?从0写到9显然太过低效,所以元字符-就可以用来表示区间范围,利用/[0-9]/就能匹配所有的数字, /[a-z]/则可以匹配所有的英文小写字母。注意,正则表达式是严格区分大小写的。

const reg = /[0-9]/
const reg1 = /[a-z]/
const a = '8'.replace(reg,'HELLO')  //HELLO
const b = 't'.replace(reg1,'HELLO') //HELLO
const c = 'T'.replace(reg1,'HELLO') //T,因为匹配不到,所以replace没有替换

那如果我们就想匹配数字和-呢?很简单,用\来转义-使他失去他自己的特殊含义就可以啦

const reg = /[0-9|\-]/g
const d = '2020-01'.replace(reg,'H') //HHHHHHH

请问你学废了吗?

除了在集合中用-对要匹配的字符范围进行定义,我们还有更简便的方式,使用正则表达式中的元字符。

常用元字符定义如下:

感谢掘友@scq000提供的记忆图 文章传送门:juejin.cn/post/684490…

image.png

下面例子没有全局匹配,只匹配到第一个符合条件的就停止匹配

const reg = /\d/
const a = '8'.replace(reg,'HELLO')  //HELLO 
const reg1 = /\D/
const b = '8'.replace(reg1,'HELLO')  //8   
const reg2 = /\w/
const c = 't'.replace(reg2,'HELLO') //HELLO
const reg3 = /\W/
const d = 't'.replace(reg3,'HELLO') //t

量词

现在如果需要匹配一个连续出现10次数字的字符串,按照我们上面的学过的,可以用

const reg = /\d\d\d\d\d\d\d\d\d\d/

来进行匹配,那如果要匹配出现10000次呢?那岂不是要我们写到手酸眼花,这个时候就可以使用正则表达式的量词了。

感谢掘友@scq000提供的记忆图 文章传送门:juejin.cn/post/684490…

image.png

所以根据这些量词的定义,我们来实操一下怎么使用吧!

const reg = /\d{3}/
const a = '1234'.replace(reg,'HELLO')  //HELLO4
const reg1 = /\D?/
const b = '8'.replace(reg1,'HELLO')  //HELLO8   
const c = 'w'.replace(reg1,'HELLO')  //HELLO
const reg2 = /\d*/
const d = '77782837464838'.replace(reg2,'HELLO') //HELLO
const e = 'w'.replace(reg2,'HELLO') //HELLOw
const reg3 = /\d+/
const f = '77782837464838'.replace(reg3,'HELLO') //HELLO
const g = 'w'.replace(reg3,'HELLO') //w

边界

现在有这么一个字符串,'This is a student' 我们想要匹配字符串中的所有的is,那这个时候如果用/is/你会发现This中的is也被匹配到了

const reg = /is/g
const a = 'This is a student'.replace(reg,'HELLO') //ThHELLO HELLO a student

这个时候就需要我们的边界字符出场了,常见的边界字符有

image.png

举个例子,我们要匹配is,那这个is左右都是单词的边界,所以

const reg = /\bis\b/g
const a = 'This is a student'.replace(reg,'HELLO') //This HELLO a student

那如果我们要匹配This中的is呢?

const reg = /\Bis\b/g
const a = 'This is a student'.replace(reg,'HELLO') //ThHELLO is  a student

所以你可以理解\b和\B就是来限定要匹配的字符是不是在我们单词的最边边就可以了。

如果有这么一个字符串 thisisthebest,我们只想匹配this中的th,你可以用^来限定匹配的字符就是整个字符串的开头

const reg = /^th/
const a = 'thisisthebest'.replace(reg,'HELLO') //HELLOisisthebest

$也是类似的,限定匹配的字符在整个字符串的结尾

const reg = /test$/
const a = 'testisatest'.replace(reg,'HELLO') //testisaHELLO

const reg2 = /^test./
const b = 'testisatest'.replace(reg2,'HELLO') //HELLOsatest

最后看下我们上面的例题:

const reg = /\d\B\D\w{2}\W{2}\s\S\b/
const a = '1a_a(& s'.replace(reg,'匹配成功')

上述的正则表达式翻译过来就是: 数字1个 非单词边界 非数字1个 字母2个 非字母2个 空白字符1个 非空白字符一个 单词边界

结果就是匹配成功~~~

子表达式

那么正则最基本的我们已经大概都学完了,是不是需要来点面试题开开胃? 请写出一个正则表达式,支持匹配如下三种日期格式:

2016-06-12

2016/06/12

2016.06.12

这个时候可能就会有人写出了类似这样的正则

const reg = /\d{4}[\-\/\.]\d{2}[\-\/\.]\d{2}/

很遗憾的告诉你,这个答案是错的。

const reg = /\d{4}[\-\/\.]\d{2}[\-\/\.]\d{2}/
const a = '2020-02-02'.replace(reg,true)  //true
const b = '2020-02/02'.replace(reg,true)  //true

题目中明显要求是要有统一的分隔字符,所以不符合题目要求。既然要有统一的分隔字符,就要求能有一种机制来捕获第一个分隔符,然后第二个分隔符与其保持一致。此时就需要反向引用了。

分组

()可以达到分组的功能,使量词作用于分组,()包括的表达式就可以认为是一个组,例如:

const reg = /(is){3}/g
const a = 'isisisis'.replace(reg,true)  //trueis

如果分组只用来做匹配,那只能发挥他功能的百分之十,更大的作用的用来做反向引用。

反向引用

回到我们的题目,匹配类似于2016-06-12,2016/06/12这样的日期字符串,还有一个隐藏的条件,就是我月和日之间的分隔符需要和第一个分隔符保持一致,不然就会出现2016-02/09这样的字符串也被匹配上,那这个时候就应该使用到反向引用,也就是在我们的正则的后面部分需要引用前面已经匹配到的子字符串。你可以把它想象成是变量,反向引用的语法像\1,\2,....,其中\1表示引用的第一个子表达式(第一个分组匹配到的),\2表示引用的第二个子表达式(第二个分组匹配到的),以此类推。而\0则表示整个表达式。

所以匹配2016-06-12,2016/06/12,2016.06.12这三种日期的正则表达式我们可以写成:

| 表示或,表示匹配其中的一个

/\d{4}(-|\/|\.)\d{2}\1\d{2}/

记住,反向引用的前提是先用()进行分组

很多时候,分组反向引用也常被用在字符串替换中,此时就分别用1,1,2...来代替匹配到的表达式

例如,我们需要将2020-02-10这样的格式转换为10/02/2020这样的格式,我们就可以这么写👇

const reg = /(\d{4})-(\d{2})-(\d{2})/g
const a = '2020-02-10'.replace(reg,'$3/$2/$1')  //10/02/2020

再来一道相似的题

写一个函数,将驼峰类型的字符串转换为-连接的字符串,例如 getElementById --> get-element-by-id

先思考一分钟再看答案吧!!!

function convert(str){
    const reg = /([A-Z])/g
    return str.replace(reg,'-$1').toLowerCase() 
}


convert('getElementById')  //get-element-by-id

写一个函数,将-连接的字符串转换为驼峰类型的字符串,例如 get-element-by-id --> getElementById

先思考一分钟再看答案吧!!!

function conversion(str) { 
  return str.replace(/-(\w{1})/g, ($1,$2)=>
  	 $2.toUpperCase()
  )
}
conversion('this-is-good')

先一个判断电话号码的正则表达式

先思考一分钟再看答案吧!!!

const reg = /^1[34578]\d{9}$/

前瞻

有这么一个场景, happyhappily字符串中我要匹配副词happily,此时我可以使用前瞻来满足我的需求

const reg = /happ(?=ily)/
const a = 'happyhappily'.replace(reg,'HELLO') //'happyHELLOily'


const reg2 = /\d+(?=%)/
const b = '50%'.replace(reg2,'XX') //XX%

正则表达式从文本头部向尾部开始解析,文本尾部方向,称为‘前’

前瞻就是正则表达式匹配到规则的时候,向文本尾部检查是否符合断言,断言部分不算进匹配的字符之内。

看不太懂?没关系,咱们看一道题,这道题也是面试常见题:

如何给数字加上千分位分隔符?

有一种toLocaleString的方法跟正则无关,大家可以去了解下,咱们来看看用正则如何秒杀。

vartoThousands = function(number) {
    return (number + '').replace(/(\d)(?=(\d{3})+$)/g, '$1,');
}

vartoThousands(123456789)

根据前瞻,/(\d)(?=(\d{3})+)/就是匹配符合右边有3的倍数个数字以上的左边这个数字的字符串,但是我们发现,它能把11231234替换成1,1231234。

这明显不是我们要的结果,我们只想要右边刚好是3的倍数个,所以我们加上$,$限定了符合前瞻这个断言的最后一个字符一定是整个字符串的最末端,也就是说11231234从头部开始匹配,判断1的时候,发现后面虽然有3x2个数字,但并不是字符串的最末尾,所以继续往后面的第二个1匹配,此时后面有3x2个数字,并且是字符串的最末尾,所以使用 /(\d)(?=(\d{3})+$)/,这个能把11231234替换成11,231234,这个结果我们比起上一步更接近我们想要的了,但右边的还没替换完成。 所以接下来我们以同样的匹配规则继续匹配右边的231234。加上g修饰符,于是有/(\d)(?=(\d{3})+$)/g,最后就是我们要的结果了。

看懂了吗?

面试真题

验证身份证号码

规则 身份证号码可能为15位或18位,15位为全数字,18位中前17位为数字,最后一位为数字或者X

function isCardNo(number) {
    var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
    return regx.test(number);
}

给 javascript 的 string 原生对象添加一个名为 trim 的原型方法,用于截取字符串前后的空白字符

String.prototype.trim = function(){
    return this.replace(/^\s*|\s*$/g,'');
} 
console.log('  love  love    '.trim())   //love  love

url的params

获取 url 中的参数

指定参数名称,返回该参数的值 或者 空字符串 不指定参数名称,返回全部的参数对象 或者 {} 如果存在多个同名参数,则返回数组

function getUrlParam(url, key) {
    var arr = {};
    url.replace(/\??(\w+)=(\w+)&?/g, function(match, matchKey, matchValue) {
       if (!arr[matchKey]) {
           arr[matchKey] = matchValue;
       } else {
           var temp = arr[matchKey];
           arr[matchKey] = [].concat(temp, matchValue);
       }
    });
    if (!key) {
        return arr;
    } else {
        for (ele in arr) {
            if (ele = key) {
                return arr[ele];
            }
        }
        return '';
    }
}

替换空格

来自leetcode : leetcode-cn.com/problems/ti…

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

输入:s = "We are happy."
输出:"We%20are%20happy."

可以直接上leetcode上面写下你的代码验证吧!

/**
 * @param {string} s
 * @return {string}
 */
var replaceSpace = function(s) {
    //正则.,每个空格都用一个%20来代替
    return s.replace(/\s/g,'%20')
};

验证是否是电子邮箱

规则定义:

• 以大写字母 [A-Z]、小写字母 [a-z] 、数字 [0-9]、下滑线 _、减号 -及点号 .开头,并需要重复一次至多次+

• 中间必须包括 @ 符号。

• @ 之后需要连接大写字母 [A-Z]、小写字母 [a-z]、数字[0-9]、下滑线_、减号-及点号.,并需要重复一次至多次+

• 结尾必须是点号.连接廏至噕位的大小写字母[A-Za-z]{2,4}

var pattern = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;

pattern.test('cn42du@163.com')  //true;
pattern.test('ifat3@sina.com.cn') // true;
pattern.test('ifat3.it@163.com') // true;
pattern.test('ifat3_-.@42du.cn') // true;
pattern.test('ifat3@42du.online') // false;
pattern.test('邮箱@42du.cn') // false;

填充函数代码使其功能完整

var str = "您好,<%=name%>。欢迎来到<%=location%>";
function template(str) {
 // your code
}
var compiled = template(str);
// compiled的输出值为:“您好,张三。欢迎来到xxx”
compiled({ name: "张三", location: "xxx" });

思考一分钟之后再看答案

var str = "您好,<%=name%>。欢迎来到<%=location%>";

function template(str) {
    return data => str.replace(/<%=(\w+)%>/g, (match, p) => data[p] || '')
}
var compiled = template(str);
compiled({
    name: "张三",
    location: "xxx"
});  // compiled的输出值为:“您好,张三。欢迎来到xxx” 

实现一个 render(template, context) 方法,将 template 中的占位符用 context 填充。

示例:

var template = "{{name}}很厉害,才{{age}}岁"
var context = {name:"bottle",age:"15"}
输入:template context
输出:bottle很厉害,才15岁
要求:

级联的变量也可以展开
分隔符与变量之间允许有空白字符

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。

以这个表达式为例:a.*b, 它将会匹配最长的以a开始,以b结束的字符串。

如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配

如果是这样子的话,就会匹配到 {{ name }}很厉害,才{{age}} 这整一段内容,不符合我们的需求

我们需要的是 {{ name }}和{{age}}这段,所以非贪婪匹配来匹配到所有的{{}}模板

也就是用 .*? 来进行匹配

const render = (template, context) => {

  const reg = /{{(.*?)}}/g
  return template.replace(reg, (match, p) => {
    return context[p.trim()] || ''
  })
}

render("{{  name   }}很厉害,才{{age}}岁", { name: "bottle", age: "15" })

当然,咱们不用非贪婪匹配也可以写

const convertTemplate = (template, context) => {
  const reg = /{{\s*(\w+)\s*}}/g
  return template.replace(reg, (match, p) => {
    return context[p] || ''
  })
}

convertTemplate("{{  name   }}很厉害,才{{age}}岁", { name: "bottle", age: "15" })

参考文章

juejin.cn/post/684490…

juejin.cn/post/684490…

juejin.cn/post/684490…

写在最后

分享我私藏的TS教程,从0到高阶全系列,点击链接,0元获取 www.yidengxuetang.com/pub-page/in…