我从“使用正则表达式获取URL的查询参数”学到了什么

在看博客的时候,不经意间看到了一道这样的面试题:如何使用正则表达式从URL上获取查询参数?html

本身思考了一下,而后尝试用正则表达式来实现这个功能,结果发现本身对正则表达式仍是不够熟悉;因而就顺着这道题目,一步一步的去深挖这里面所涉及到的知识点。es6

之后会不按期更新“我从xxx学到了什么”系列,也是给本身学到的知识进行一个阶段性的总结吧!面试

经过这边文章,你能够学到如下知识点(如下全部代码都是基于JavaScript语言)正则表达式

  1. 正则表达式的修饰符
  2. 正则表达式的贪婪模式与懒惰模式
  3. 正则表达式的分组、分组编号、分组引用
  4. 正则表达式的先行断言/先行否认断言,后行断言/后行否认断言

前提条件

URL上的查询参数能够直接从window.location.search获取,获得的字符串就是如下形式数组

?key1=value1&key2=value2markdown

因此题目就能够转换成:使用正则表达式从以上字符串获取全部的键值对。函数

如下全部的匹配字符串都是 '?name=jim&age=20&hobby=basketball'oop

预期获得的结果学习

1、name=jim
2、age=20
3、hobby=basketball
复制代码

初版

思路:url查询参数部分的字符串格式是固定的:以?开头,后面接key=val,每组键值对以&链接url

话很少说,思路有了,就直接上代码

/[?&](.*)=(.*)/g
复制代码

输出的结果

'?name=jim&age=20&hobby=basketball'
复制代码

竟然整个字符串都被匹配上了,问题究竟出在哪里?

想法是匹配以?&开头的子串,后面跟着key=val,因此写出了 (.*)=(.*),这个按理应该能匹配出name=jim这样的字符串。为何在第一个(.*)就把后面的整个字符串都匹配上了呢?想要的结果是第一个(.*)匹配到 = 就结束了,也就是key,而后第二个(.*)继续匹配val。之因此给他们都加上()就是想在=前中止匹配,但实际上却没有按照预期获得正确结果。

为何会出现这种状况呢?因而去百度了如下,结果发现,正则表达式默认元字符,量词是采用贪婪模式

什么是贪婪模式?

贪婪模式会尽量多的匹配知足条件的字符。

以下,都是会匹配最大长度字符串

.*
.+
.{1,}
.{0,} 
复制代码

正则表达式元字符,量词默认首先最大匹配字符串,这些量词有:+,*,?,{m,n} 。一开始匹配,就直接匹配到最长字符串。

以下 <h3>abd</h3><h3>bcd</h3> 经过正则表达式:/<h3>.*</h3>/g 进行匹配,获得的结果为:<h3>abd</h3><h3>bcd</h3> 也就是整个字符串都被匹配上了。

知道了正则表达式默认是贪婪模式,那么/[?&].*=.*/g为何会匹配整个字符串的缘由找到了。

问题就出如今.*,因为它会尽量多的匹配符合条件的字符串,因此它会把整个字符串都匹配上。

这时就有一个问题:有没有办法让它最小匹配呢?

答案是确定有的,可使用懒惰模式

什么是懒惰模式

和贪婪模式相反,懒惰模式是进行最小匹配。

如何转换成懒惰模式

很简单,在表示重复字符元字符,后面加多一个?字符便可。

上面的正则表达式若是想最小长度匹配,则能够这样写:/<h3>.*?</h3>/g 那么匹配出的结果为:

<h3>abd</h3>   
<h3>bcd</h3>
复制代码

分组

在一个正则表达式中,若是给子表达式加上(),就表明这部分是一个分组,整个表达式是第一个分组。

举个栗子

/[?&](.*)=(.*)/g
复制代码

其中(.*)就是一个分组。

分组有什么用?

通常分组有两个做用:一、在结果中获取特定的分组匹配结果;二、在表达式中进行引用

  1. 在结果中获取特定的分组匹配结果

    执行regex.exce(str)时,获得的结果是一个数组;以下

    假如要匹配一个年份的年月日,一个简单的正则能够是这样的

    /(\d{4})-(\d{2})-(\d{2})/g
    复制代码

    匹配字符串1993-01-01获得的结果以下

    [
        "1993-01-01",
        "1993",
        "01",
        "01"
    ]
    复制代码

    第一个是整个表达式的匹配结果,第二个是第一个分组的匹配结果,依次类推。

    那么问题来了,我怎么知道某个分组是第几个?

    分组的编号规则

    分组能够经过从左到右计算其开括号来编号。整个表达式始终是第一个分组。

    以下表达式 (A)(B(C)) 有四个分组 0 (A)(B(C)) 1 (A) 2 (B(C)) 3 (C) 在执行exec函数时,返回的结果也是按照上面的分组返回结果。 第一个返回结果是0号分组匹配到的字符串;第二个返回结果是1号分组匹配到的字符串;依次类推

  2. 在表达式中进行引用

    在一些场景中,须要匹配和前面某个子表达式匹配结果同样的结果,好比说,须要匹配重复的单词,那么就可使用分组引用。

    一样举个栗子,匹配重复的单词

    abc abc

    能够经过如下表达式进行匹配

    /\b(\w+)\b\s+\1\b/
    复制代码

    \1:引用第一个分组匹配到的结果,上面的栗子就是引用(\w+)获得结果,也就是abc

    须要注意的是,引用分组进行匹配,仅仅是引用,它并非真正的分组,因此在结果的数组中是不会有这个引用匹配到的结果。

知识点补充

JavaScript正则表达式的修饰符
  1. g:全局匹配
  2. i:忽略大小写
  3. m:多行匹配
  4. s:使用dotAll模式匹配
  5. y:“粘连”匹配

以上修饰符,若是有不理解的,能够去ES入门教程-正则表达式学习下,这里就再也不赘述了。

JavaScript正则表达式的元字符

不熟悉的能够去菜鸟教程学习下,这里就再也不赘述了。

第二版

通过初版的惨痛失败,意识到了要使用懒惰模式进行匹配,因此改进如下获得第二版表达式

[?&]?(.*)?=(.*)?
复制代码

输出结果

1、?name=
2、jim&age=
320&hobby=
复制代码

一共匹配到了三个,但结果仍是不是我预期的,

问题在哪里呢?

[?&]?:匹配上的字符串以?或&或都没有开头

.*?:匹配任意字符串,匹配数可使0或者1,也就是说能够是什么都不匹配

.*?=.*?:第一个.*?会匹配到=前的字符串,而第二个.*能够什么都不匹配,由于它是懒惰模式

并且从预期的结果来看,在匹配结果中,并不须要?&。仅仅是想匹配?或&后面的ke=val。

这时候,后向断言就能够发挥做用了。

什么是反向断言

这个名字读起来就很别扭,不用去理解它的具体含义,权当是一个名字就行。

语法

(?<=exp)x
复制代码

断言部分在()内,它的做用是匹配知足x的字符串时,校验x前面的字符串是否知足exp,若是知足,则匹配,不然不匹配。

举个具体的栗子

只匹配在#后面的数字

const reg = /(?<=#)\d+/g
const str = '#90abc'

str.match(reg)
// [90]
复制代码

反行否认断言

语法

(?<!=exp)x
复制代码

它的做用是匹配知足x的字符串时,校验x前面的字符串是否知足exp,若是不知足,则匹配,不然不匹配。

举个具体栗子

不匹配#后面的数字

const reg = /(?<!#)\d+/g
const str = '#90abc$100edf%102ooo'

str.match(reg)
// [0, 100, 102]
复制代码

正向断言

语法

x(?=exp)
复制代码

它的做用是匹配知足x的字符串时,校验x后面的字符串是否知足exp,若是知足,则匹配,不然不匹配。

举个具体栗子

只匹配百分号前的数字

const reg = /d+(?=%)/  
const str = '90%'

str.match(reg)
// [90]
复制代码

正向否认断言

语法

x(?!exp)
复制代码

它的做用是匹配知足x的字符串时,校验x后面的字符串是否知足exp,若是不知足,则匹配,不然匹配。

举个具体栗子

只匹配不在百分号前的数字

const reg = /d+(?!%)/g  
const str = '100$'

str.match(reg)
// [100]
复制代码

终版

又多了一件厉害的装备,如今是时候祭出最终版的表达式了

/(?<=[?&])[^&]*/g
复制代码

输出结果

1、name=jim
2、age=20
3、hobby=basketball
复制代码

(?<=[?&]):后向断言,匹配后面的表达式以后,再次进行验证,若是都知足,则匹配

[^&]*:匹配不是&的任意字符

总结

  1. 默认匹配模式是贪婪模式,若是要转换成懒惰模式,则在元字符或量词后加 ?
  2. 分组编号规则,设置分组后的到的匹配结果,以及分组的两个使用场景
  3. 断言能够额外新增匹配条件,断言内匹配上的表达不会出如今最终匹配结果中