浅谈JavaScript正则表达式

栏目: JavaScript · 发布时间: 5年前

内容简介:刚开始学习 JS 时,正则表达式一直是我不愿意面对的,每次读到有关正则表达式的时候,都会避而远之。可是,一次,当我打开 JQ 源码的时候,发现里面有大量的正则表达式。于是乎,自己就强迫自己学习正则,学习的过程还是蛮愉快的。最后,真香定律终于出现了。哈哈哈!!这篇教程我会由浅入深的来和大家分享正则表达式,让大家即学习到正则表达式的用法,也了解其在 JS 中表现的不为人知的一面。正则表达式是 JS 中很重要的一环。也是对很多人比较不愿意面对的一个知识点。但是,当我们真正掌握了正则表达式,可以利用其在我们的代码

刚开始学习 JS 时,正则表达式一直是我不愿意面对的,每次读到有关正则表达式的时候,都会避而远之。可是,一次,当我打开 JQ 源码的时候,发现里面有大量的正则表达式。于是乎,自己就强迫自己学习正则,学习的过程还是蛮愉快的。最后,真香定律终于出现了。哈哈哈!!

这篇教程我会由浅入深的来和大家分享正则表达式,让大家即学习到正则表达式的用法,也了解其在 JS 中表现的不为人知的一面。

概述

正则表达式是 JS 中很重要的一环。也是对很多人比较不愿意面对的一个知识点。但是,当我们真正掌握了正则表达式,可以利用其在我们的代码中发挥很大的威力,大大的简化我们的代码。对于喜欢阅读一些库源码的伙伴。这个真的是必须掌握的。

当然,正则基本在每个语言都有实现。虽不能说都相同。但是基本上都是大同小异。此外,正则表达式的范围非常的广,这里也不可能每个知识点都会涉及到。这里,作者会将一些我认为常见的,重要的,常见的注意点给大家一一分析。

正则表达式基础概念

定义

正则表达式,英文叫做 Regular Expression。按照英文字面意思解释,就是有 规则 的表达式,正则表达式就是由一些列的语法规则组成的字符串,然后按照这种组合的规则去匹配一些字符串,筛选出符合条件的字符串。

知识了解:根据 ECMA5 规范,JS 中正则表达式的格式和功能是以 perl 语言为蓝本的。

我们平时写的正则表达式很不直观,这里推荐一个在线工具。该 工具 以可视化的界面来描述我们写的正则表达式。工具比较简单,大家自行了解。在线工具

正则表示方法的区别

正则表达式的表示方法有两种

  1. 字面量表示法
  2. 构造函数表示法
let reg = /text/ig

    let regExp = new RegExp('text', ig)

复制代码

两种表示方法都可行。区别在于:利用构造函数进行表示的时候,可以动态的构建正则表达式的规则。

let str = String('****')
    // 如 let className = str + 'name'
    regExp = new RegExp(className, 'ig')

复制代码

正则表达式的组合规则太多了,大家可以去看一下 ECMA 规范。下面我们就介绍一些常用的,经常遇到的情况。

正则表达式组成元素

正则表达式的组成一般由以下几类构成

  1. 原义文本字符
  2. 元字符
  3. 修饰词

修饰词

在 JavaScript 中,全局修饰词有 g、i、m、y、u、s

这些修饰词在正则表达式的匹配中,起到了很重要的作用。

g: 代表全局匹配,当匹配到目标字符串的时候,不会停止匹配,而会继续匹配。直到匹配结束为止。

i: 匹配的过程中,忽略大小写

m: 换行匹配。字符串可以换行,如果当前行没有匹配到,可以换行继续匹配

y: 执行“粘性”搜索,匹配从目标字符串的当前位置开始

u: 相当于将匹配模式转换成 unicode 模式,可以正确处理大于\uFFFF的 Unicode 字符。大家可以自行参考 了解

s: 我们知道 元字符 . 代表除了回车换行符以外的所有字符,但是加上 s 修饰符后, . 可以匹配任意字符

/./.text('\n')   // false
    /./s.test('\n')  // true

    //其实还有另一种技巧可以匹配所有字符
    /[^]/.test('\n') // true
复制代码

y 代表什么含义? 这个等会再做解释,先简单说下,这个与正则表达式的一个属性 lastlndex 有关系,现在解释,可能会一脸懵逼。 下面会与 g 标志 一起讨论。

元字符

元字符是在正则表达式中有 特殊含义 的非字母字符。这些元字符使得正则表达式的组合规则十分强大。

// 引用自规范原文 我们可以看一下这些元字符都有哪些,我们应该很熟悉这其中代表的含义。
PatternCharacter :: SourceCharacter but not any of:
^ $ \ . * + ? ( ) [ ] { } |

此外,还有一些字符,是组合而成的,代表特殊的含义
有 \b、\B、\d、\D、\w、\W、\s、\S、\f、\v、\t、\n、\r 等等。这些字符代表的含义,大家自行了解,这里不一一讲解这些元字符的含义。
下面举例子会用到一些,会对其含义做说明。
复制代码

既然元字符代表这么多的含义,那么我们如果需要在字符串中匹配这些字符怎么办呢? 这个时候,可以使用转义字符帮忙。

举个栗子:

reg = /\d/ // \d 是元字符,表示数字,这个正则表达式只能匹配数字,如果我们需要匹配'\d'呢
    reg.test('\d') // false
    这时候需要用转义字符转义
    reg = /\\d/ // \ 代表转义字符
    reg.test('\d') // 还是false
    这个为什么还是 false 呢,不是按照规矩办事吗?
    这是因为 JS 里的字符串也有转义字符!
    可以试一下 '\d'.length 等于1,这个时候要想匹配 '\d' 必须要在字符串中对其转义
    reg.test('\\d') // true

复制代码

总结:遇到这种情况,别总是忙着为正则表达式转义,还得为字符串转义,关于字符串里面的转义字符,这超过了本篇讨论范围,大家自行了解。

常见元字符的解释

下面介绍一些常用的元字符

元字符 表示及含义 解释
. /[ ^\r\n ]/ 除了回车换行以外的全部字符
\d /[0-9]/ 数字字符
\D /[^0-9]/ 非数字字符
\w /[0-9a-zA-Z_]/ 单词字符(数字,字母,下划线)
\W /[^0-9a-zA-Z_]/ 代表非单词字符
\s /[\f\n\r\t\v\u00a0\u1680\u180e
\u2000-\u200a\u2028\u2029\u202f
\u205f\u3000\ufeff]/
空白字符,包括空格、
制表符、换页符和换行符
\S /[^\s]/ 非空白字符
| /x|y/ x or y
\b word boundary 单词边界
\B none word boundary 非单词边界
^ /[^abc]/ 匹配以abc为开始的字符串
$ /[abc$]/ 匹配以abc为结束的字符串

字符类

我们都知道用特定的正则表达式去匹配特定的字符串很轻松。因为不会出现其他情况,逻辑上讲是非常清晰的事情。

举个栗子:

let reg = /abc\b/
    // 如下图 表示匹配 abc后面跟上单词边界。
    reg.test('abc bcd') // true
    reg.test('abcc') //false 因为abc后面紧跟单词边界

复制代码
浅谈JavaScript正则表达式

可是,有时候我们的需求不是匹配特定字符,而是要匹配符合一些特性的字符。比如,需要匹配 a b c 任意一个,存在即满足条件。

简而言之:我只要你们中的一个出现就OK。

这个时候,我们就可以使用 元字符 [] 来构建这样一个 字符类

这里的类,我们可以联想到编程语言的类,泛指一些符合特性的事物,而不是特指。

举个例子:

let reg = /[abc]\b/
    // 如下图 表示 one of abc 后面紧跟单词边界就满足条件
    reg.test('a') // true

复制代码
浅谈JavaScript正则表达式

字符类很强大,但是,如果我们的需求是要匹配除了一个字符类之外的字符呢?

简而言之:别人都行,就你们不可以。

这个时候,我们可以使用 字符类的反向类 ,使用元字符 ^ 在写好的字符类里面取反。

举个例子:

let reg = /[^abc]\b/
    // 如下图 表示 None of abc 后面紧跟单词边界就满足条件
    reg.test('e') // true

复制代码
浅谈JavaScript正则表达式

解释一下单词边界的含义:单词边界这个概念,很多人都比较模糊。我只能说一下我的理解 在正则表达式中,\w 代表单词字符,\W 代表非单词字符,只要单词字符紧挨着非单词字符,那么在这两者中间,就存在单词边界。

范围类

字符类给我们注入了一种全新的功能,类似于数据库的模糊查询。我们可以利用这一功能匹配范围内的字符串了。 但是,应用场景多了,也会出现问题。

举个例子:

//我们想要匹配 数字1到8中的任意一个,我们利用字符类的概念可以这样写
    reg = /[12345678]/
    // 可能有的小伙伴可以接受,那好,如果我们想要匹配小写字母,a 到 z 的任意一个字符
    reg = /[abcdefghijklmnopqrstuvwxyz]/ // 这样一坨,写的很难受

复制代码

看上面的例子,写代码的难受,读代码的估计也不舒服。这时候,我们需要范围类帮忙。

所谓的范围类,就是匹配具有一定规则的一段范围之内的字符:

使用字符 - ,来达成这一目标

举个例子:

// 匹配 a 到 z 的任意一个字符
    reg = /[a-z]/
    // 匹配除了 a 到 z 的任意一个字符
    reg = /[^a-z]/

    // 常见的模式
    /\d/ 就相当于 /[0-9]/
    /\D/ 就相当于 /[^0-9]/
    /\w/ 就相当于 /[a-zA-Z0-9]/
    /\W/ 就相当于 /[^a-zA-Z0-9]/

复制代码
浅谈JavaScript正则表达式

一个问题:**-**不是元字符,是否可以在范围类中匹配?如果可以,是否需要转义或者其他特殊操作。

匹配该字符在字符类中是可以的,但是有注意点:即 - 只可以放在范围类的开头或结尾,才会匹配该字符。

不可以出现在两个字符中间,不然,该字符还是会被当作范围类中的特殊字符来对待

举个例子:

let reg = /[1-z]/
    reg.test('-') // false
    reg.test('a') // true

    let reg = /[1-9-]/
    reg.test('-') // true
    reg.test('1') // true

复制代码
浅谈JavaScript正则表达式

量词

我们之前介绍的字符类或非字符类,只能匹配特定类出现一次,如果出现多次,需要额外再写相同的代码进行匹配。

举个例子:

// 需求:匹配有连续5个数字的字符串
    let reg = /\d\d\d\d\d/ // 那如果要匹配出现连续3至6个数字的字符串呢?
    let reg = /\d\d\d|\d\d\d\d|\d\d\d\d\d|\d\d\d\d\d\d/ // 这样写太复杂,我需要更简单的写法

复制代码

有这样的需求时,我们就需要量词来帮忙。

量词有几种表示的方式,各自代表不同的需求。

量词的表示有以下这几种表示:

+表示匹配一次或一次以上。one or more

?表示匹配0次或一次。 one or less

*表示匹配0次或0次以上。none or onemore

{m,n}表示匹配 m 到 n 次。[m,n]闭区间 m less n most

{m,}表示匹配至少 m 次。 m less

{m}表示匹配出现 m 次

如果要表示至多出现 m 次,可以这样表示 {0,m}

重写例子:

reg = /\d{5}/ // 匹配有5个连续数字的字符串
    reg = /\d{3,6}/ // 匹配有连续 3 至 6 个数字的字符串

复制代码

大家可以在图形化工具里面自己尝试下,很直观。

正则表达式的贪婪模式

注意:这里,我们会先应用 String.prototype.replace 方法来很形象的解释正则表达式的贪婪模式。

来看一下这样等应用场景:我们需要匹配连续 5-10 个小写字母等场景。目标字符串满足这个需求,但是匹配等结果是什么?

是匹配到5个字母就不匹配还是继续匹配更多等字母直到匹配失败呢?

人是贪婪的,所以人设计的正则表达式也是贪婪的。

在正则表达式中,会尽可能的匹配更多的字符,直到匹配失败为止。

举个例子:

reg = /[a-z]{5,10}/
    str = 'ahhsjkiosbsasdasllk' // str.length === 19
    我们用 replace 方法来验证一下,正则表达式匹配了多少字符
    str1 = str.replace(reg, 'Q') // str1.length === 10
    str1 = 'Qsasdasllk' 

复制代码

上述的例子,我们可以看出,正则表达式是属于贪婪模式。那么我们现在想要取消贪婪模式,可以吗?

可以,只需要在 量词 后加上**元字符?**就可以取消贪婪模式啦。

举个例子:

reg = /[a-z]{5,10}?/
    str = 'ahhsjkiosbsasdasllk' // str.length === 19
    我们用 replace 方法来验证一下,正则表达式匹配了多少字符
    str1 = str.replace(reg, 'Q') // str1.length === 15
    str1 = 'Qkiosbsasdasllk' 

复制代码

分组

假如,我们现在需要这样一个需求,需要匹配包含'mistyyyy'连续重复2次的字符串。

这种情况,我们按照之前的写法可能会这样写。

/mistyyyy{2}/

复制代码

但是,这样匹配是错误的,这表达的意思是y重复2次,而不是 mistyyyy 重复2次

这个时候,我们可以使用分组这个概念来帮助我们。

用法:将需要分组的信息,用元字符()包含起来。这样就可以使 量词作用于分组 了。

reg = /(mistyyyy){2}/
    str = 'Im mistyyyymistyyyymistyyyy yeah'
    reg.test(str) // true
    如下图所示

复制代码
浅谈JavaScript正则表达式

再看一个例子,这时候,我要改名字了。mistyyyy 或者 missyyyy 都是可以的。那我们怎么匹配它呢?

//我们可以这样写
    reg = /mistyyyy|missyyyy/
    但是利用分组,我们可以这样写
    reg = /mis(s|t)yyyy/
    reg.test('mistyyyy') // true
    reg.test('missyyyy') // true

复制代码

捕获和非捕获分组

现在我们来看一个,平时开发中经常出现的需求。如:将日期 '2018-12-23' 转换为 '23/12/2018'

这个时候,我们就很头大了。单纯的匹配到这个日期并不困难。但是如何将其转换这就成了难点。当然,我们可以进行最原始的方法进行解决。

reg = /\d{4}-\d{2}-\d{2}/
    '2018-12-23'.replace(reg, '23/12/2018')
    // 这样的程序基本没有灵活性。

复制代码

这时候,我们要讲的捕获就要出现了。前面讲到了分组,既然可以分组,那我们也可以捕获分组。

捕获分组又可以称为引用:

  1. 一种是正向引用,用于正则表达式里面的表示。
  2. 另一种称为反向引用,常用于正则表达式匹配结果的替换。

我们先看正向引用,举个最适合的例子。

//我们现在需要匹配一个 dom 节点,比如匹配 id 为 container 的 div dom 节点。

    let domContainer = 
    `<div>
        <div id="container">
            this is container
        </div>
    </div>`
    reg = /<div id="container">([^<\/]+)<\/div>/m
    domContainer.replace(reg, 's') // <div>s</div>

复制代码

这个时候,我们可以使用正向引用,即 \1 代表第一个分组的引用, \2 代表第二个分组的引用等等 以此类推

我们来重写正则表达式。

reg = /<(div) id="container">([^<\/]+)<\/\1>/m
    // 大家可以试一下,同样的效果。

复制代码

介绍完正向引用,我们来看一下反向引用。

在正则表达式进行分组时,当匹配结束时候,我们希望可以以分组为单位进行字符串的替换,这样可行吗?

举个例子

reg = /(\d{4})-(\d{2})-(\d{2})/
    // 这样我们就把正则写好了。考虑到之前的例子,我们需要将第三个分组放在开头,第二个分组位置不变,第一个分组放在最后
    // 如何做
    '2018-12-23'.replace(reg, '$3/$2/$1') // "23/12/2018"

复制代码

由上述例子可以得知,反向引用就是用 '$1' 获取第一个分组 '$2' 获取第二个分组...以此类推

注意;是 '$1' 代表一个分组,而不是 $1,这里需要注意一下

有时候,我们根本不需要捕获一个分组,就如刚才 reg = /mis(s|t)yyyy/ 一种情况。我们只是想用分组实现一下 或 操作。 没有分组的必要,其次,当正则表达式变得复杂起来,保持明显的分组是很有必要的。

这个时候,我们可以在分组的括号里面加上 ?: 这样就可以取消捕获了

reg = /mis(s|t)yyyy/
    'mistyyyy'.replace(reg, '$1') // 't'
    // 取消捕获
    reg = /mis(?:s|t)yyyy/
    'mistyyyy'.replace(reg, '$1') // '$1'

复制代码

我们可以看下图片的比较,没有分组了。说明取消了捕获。

浅谈JavaScript正则表达式
浅谈JavaScript正则表达式

正向匹配和反向匹配

先解释这两个概念,我们都知道在 JavaSCript 中,正则表达式匹配的顺序是顺着目标字符串进行匹配。

如果我们需要设计一些带条件的匹配规则,比如说:我们需要匹配字符串 'mistyyy' 后面必须是 'good'

举个例子:

reg = /mistyyyygood/    

复制代码

这个时候,'mistyyyy' 后面是 'good' 但是此时,'good' 也被匹配到了,如果我们用 replace 做替换,那么 good 也会被替换掉。

要满足这样的条件。我们可以使用正向匹配

规则如下:

  1. /expression(?=condition)/ 这表示expression后面必须紧跟 condition 作为条件。
  2. /expression(?!condition)/ 这表示expression后面必须不是 condition 作为条件。

举个例子;

str = 'mistyyyygood boy'
    /mistyyyy(?=good)/.test(str) // true
    str.replace(/mistyyyy(?=good)/, 'you') // yougood boy

    /mistyyyy(?!good)/.test(str) // false

复制代码

注意:此时,condition 只是作为条件进行筛选,并不会被匹配到。

我们可以看一个例子

str = 'mistyyyygood boy'
    str.replace(/mistyyyy(?=good)/, '$1') // $1good boy

    // 我们可以看出条件是不会被匹配到到。

复制代码

反向匹配,与正向匹配的规则相反,该特性是 ES 2018 新加的特性。

规则如下:

  1. /(?<=condition)expression/ 表示 expression 前必须满足 condition 条件才匹配
  2. /(?<!condition)expression/ 表示 expression 前必须不满足 condition 条件才匹配

详解 regExp 对象属性

我们随便写一个正则,看一下打印出来的正则表达式的属性有哪些

reg = /\u0002/yimgus
    {
        dotAll: true,
        flags: 'gimsuy',
        global: true,
        ignoreCase: true,
        lastIndex: 0,
        multiline: true,
        source: '\u0002',
        sticky: true,
        unicode: true,
        __proto__: Object
    }
    
复制代码

dotAll,global,ignoreCase,multiline,stricky,unicode 分别代表修饰词 s,g,i,m,y,u 是否出现在正在表达式的修饰词位置。

flags 表示出现的修饰词。

source 表示正则表达式的规则主体部分。

lastIndex 个人认为最重要的属性就是该属性。下面会围绕该属性展开拓展一下。

我们先看个奇怪的例子:

reg = /\d{2}/g
    str = '12sd'
    reg.lastIndex // 0
    reg.test(str) // true
    reg.lastindex // 2
    reg.test(str) // false
    reg.lastindex // 0

复制代码

lastIndex的值类型是 number 类型。可以进行读写操作。

举个例子:

reg = /\d{2}/
    reg.lastindex // 0
    reg.lastIndex = 2
    reg.lastindex // 2

复制代码

该属性的含义是从目标字符串的 lastIndex 位置开始进行匹配。但是这是有限制的, 只有当修饰符存在 g 或者 y 的时候,才会起作用

举个例子:

reg = /\d{2}/
    str = '12sd'
    reg.lastIndex = 2
    reg.test(str) // true
    reg.lastIndex // 2

    reg =/\d.\d/s
    str = '1\n2'
    reg.lastIndex = 2
    reg.test(str) // true
    reg.lastIndex // 2

    reg = /\d{2}/g
    str = '12sd'
    reg.lastIndex = 2
    reg.test(str) // false
    reg.lastIndex // 0

    reg = /\d{2}/y
    str = '12sd'
    reg.lastIndex = 2
    reg.test(str) // false
    reg.lastIndex // 0

复制代码

通过以上的例子,我们可以看出,lastIndex 属性只影响了 修饰符 g 和 修饰符 y 的匹配结果。

也就是说:只有这两种的形式是从目标字符串的 lastIndex 位置进行匹配的,其他的修饰符会忽略这个属性。

而且,这两种修饰符匹配失败了,lastIndex 会重置为0。

由此可见,g 修饰符 和 修饰符 y 有相同的作用。那么我们来探寻一下他们的异同点。

相同点:

  1. 他们都会受 lastIndex 的属性值影响正则开始匹配的效果。即影响从目标字符串的何处开始匹配
  2. 他们在匹配失败后 lastindex 的属性都会置为 0。
  3. 他们匹配成功后,lastIndex 都会重置为匹配成功的字符串的下一个字符。
  4. 修饰符 y 和 g 都不受元字符 ^ 从目标字符串开始的限制,它只受限于 lastIndex 的位置开始匹配。并且只从 lastIndex 的位置开始匹配。
reg1 = /\d/g
    reg2 = /\d/y
    str = '1ssss'
    reg1.lastIndex = reg2.lastIndex = 1
    reg1.test(str) // false
    reg2.test(str) // false
    reg1.lastindex // 0
    reg2.lastindex // 0
    // 说明都受 lastIndex 的影响,且匹配失败都会置为0

    reg1.test(str) // true
    reg2.test(str) // true
    reg1.lastIndex // 1
    reg2.lastIndex // 1
    // 匹配成功后,lastIndex 都会重置为匹配成功的字符串(chharAt(0))的下一个字符

    reg1 = /^\d/g
    reg2 = /^\d/y
    str = '1ssss1'
    reg1.lastIndex = reg2.lastIndex = 1
    reg1.test(str) // false
    reg2.test(str) // false

复制代码

不同点:

修饰符 g 是全局匹配,即匹配到目标字符串不会停止,继续匹配下去,直到没有找到所有符合规则的为止。修饰符 y 不是全局匹配,找到符合规则的就会停止。

举个例子:

reg1 = /\d/g
    reg2 = /\d/y
    str = '1sssss1'
    reg1.test(str) // true
    reg1.lastIndex // 1
    reg1.test(str) // true
    reg1.lastIndex // 7

    reg2.test(str) // true
    reg2.lastindex // 1
    reg2.test(str) // false
    reg2.lastindex // 0
    // 说明 修饰符 g 是全局匹配,而 y 不是全局匹配。

复制代码

总结

关于正则表达式,我们讲了一些常见的语法和一些比较生涩的疑难点。对于正则表达式,我们掌握了这些知识点,并不能完全发挥其应有的实力。

我们还应该掌握,应用正则的一些方法有:String 类型的 split,replace,match,search 。RegExp 类型的 test,exec 方法。

真正了解这些方法的应用,才能让正则表达式的强大威力。这些方法,这里暂时不讲了,小伙伴们应该熟悉这些方法的使用,利用他们组合正则表达式,展示出强大的威力。

此外,正则表达式常见的应用场景有模版的解析,dom节点的提取分离,这里面含有大量复杂的正则表达式。如果小伙伴需要精通掌握正则表达式,可以阅读 sizzle 这个库,该库是 JQ 的核心部分,专门处理复杂的 dom selector,希望我们可以继续努力,将正则表达式真正的掌握。这样,我们就可以写出更加优雅,更加具有可读性的代码了。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

付费:互联网知识经济的兴起

付费:互联网知识经济的兴起

方军 / 机械工业出版社 / 2017-6-1 / CNY 59.00

关于互联网知识付费的首部作品 知识工作正在被重塑,知识经济正在开启互联网时代下半场 为你展现互联网知识经济全景大图,解读新物种的前世今生 内容简介 一个产业解读 三个分析工具 一组知识卡片 书是最早的知识载体,已有2000多年的付费历史,随着移动互联网的普及,新的知识经 济在今天爆发,知识的创造者和传播者从书后走到了书前,互联网知识经济正在拉开帷幕。知识的......一起来看看 《付费:互联网知识经济的兴起》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码