浅谈JavaScript正则表达式
栏目: JavaScript · 发布时间: 5年前
内容简介:刚开始学习 JS 时,正则表达式一直是我不愿意面对的,每次读到有关正则表达式的时候,都会避而远之。可是,一次,当我打开 JQ 源码的时候,发现里面有大量的正则表达式。于是乎,自己就强迫自己学习正则,学习的过程还是蛮愉快的。最后,真香定律终于出现了。哈哈哈!!这篇教程我会由浅入深的来和大家分享正则表达式,让大家即学习到正则表达式的用法,也了解其在 JS 中表现的不为人知的一面。正则表达式是 JS 中很重要的一环。也是对很多人比较不愿意面对的一个知识点。但是,当我们真正掌握了正则表达式,可以利用其在我们的代码
刚开始学习 JS 时,正则表达式一直是我不愿意面对的,每次读到有关正则表达式的时候,都会避而远之。可是,一次,当我打开 JQ 源码的时候,发现里面有大量的正则表达式。于是乎,自己就强迫自己学习正则,学习的过程还是蛮愉快的。最后,真香定律终于出现了。哈哈哈!!
这篇教程我会由浅入深的来和大家分享正则表达式,让大家即学习到正则表达式的用法,也了解其在 JS 中表现的不为人知的一面。
概述
正则表达式是 JS 中很重要的一环。也是对很多人比较不愿意面对的一个知识点。但是,当我们真正掌握了正则表达式,可以利用其在我们的代码中发挥很大的威力,大大的简化我们的代码。对于喜欢阅读一些库源码的伙伴。这个真的是必须掌握的。
当然,正则基本在每个语言都有实现。虽不能说都相同。但是基本上都是大同小异。此外,正则表达式的范围非常的广,这里也不可能每个知识点都会涉及到。这里,作者会将一些我认为常见的,重要的,常见的注意点给大家一一分析。
正则表达式基础概念
定义
正则表达式,英文叫做 Regular Expression。按照英文字面意思解释,就是有 规则 的表达式,正则表达式就是由一些列的语法规则组成的字符串,然后按照这种组合的规则去匹配一些字符串,筛选出符合条件的字符串。
知识了解:根据 ECMA5 规范,JS 中正则表达式的格式和功能是以 perl 语言为蓝本的。
我们平时写的正则表达式很不直观,这里推荐一个在线工具。该 工具 以可视化的界面来描述我们写的正则表达式。工具比较简单,大家自行了解。在线工具
正则表示方法的区别
正则表达式的表示方法有两种
- 字面量表示法
- 构造函数表示法
let reg = /text/ig let regExp = new RegExp('text', ig) 复制代码
两种表示方法都可行。区别在于:利用构造函数进行表示的时候,可以动态的构建正则表达式的规则。
let str = String('****') // 如 let className = str + 'name' regExp = new RegExp(className, 'ig') 复制代码
正则表达式的组合规则太多了,大家可以去看一下 ECMA 规范。下面我们就介绍一些常用的,经常遇到的情况。
正则表达式组成元素
正则表达式的组成一般由以下几类构成
- 原义文本字符
- 元字符
- 修饰词
修饰词
在 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后面紧跟单词边界 复制代码
可是,有时候我们的需求不是匹配特定字符,而是要匹配符合一些特性的字符。比如,需要匹配 a b c 任意一个,存在即满足条件。
简而言之:我只要你们中的一个出现就OK。
这个时候,我们就可以使用 元字符 [] 来构建这样一个 字符类 。
这里的类,我们可以联想到编程语言的类,泛指一些符合特性的事物,而不是特指。
举个例子:
let reg = /[abc]\b/ // 如下图 表示 one of abc 后面紧跟单词边界就满足条件 reg.test('a') // true 复制代码
字符类很强大,但是,如果我们的需求是要匹配除了一个字符类之外的字符呢?
简而言之:别人都行,就你们不可以。
这个时候,我们可以使用 字符类的反向类 ,使用元字符 ^ 在写好的字符类里面取反。
举个例子:
let reg = /[^abc]\b/ // 如下图 表示 None of abc 后面紧跟单词边界就满足条件 reg.test('e') // true 复制代码
解释一下单词边界的含义:单词边界这个概念,很多人都比较模糊。我只能说一下我的理解 在正则表达式中,\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]/ 复制代码
一个问题:**-**不是元字符,是否可以在范围类中匹配?如果可以,是否需要转义或者其他特殊操作。
匹配该字符在字符类中是可以的,但是有注意点:即 - 只可以放在范围类的开头或结尾,才会匹配该字符。
不可以出现在两个字符中间,不然,该字符还是会被当作范围类中的特殊字符来对待
举个例子:
let reg = /[1-z]/ reg.test('-') // false reg.test('a') // true let reg = /[1-9-]/ reg.test('-') // true reg.test('1') // true 复制代码
量词
我们之前介绍的字符类或非字符类,只能匹配特定类出现一次,如果出现多次,需要额外再写相同的代码进行匹配。
举个例子:
// 需求:匹配有连续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 如下图所示 复制代码
再看一个例子,这时候,我要改名字了。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') // 这样的程序基本没有灵活性。 复制代码
这时候,我们要讲的捕获就要出现了。前面讲到了分组,既然可以分组,那我们也可以捕获分组。
捕获分组又可以称为引用:
- 一种是正向引用,用于正则表达式里面的表示。
- 另一种称为反向引用,常用于正则表达式匹配结果的替换。
我们先看正向引用,举个最适合的例子。
//我们现在需要匹配一个 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 中,正则表达式匹配的顺序是顺着目标字符串进行匹配。
如果我们需要设计一些带条件的匹配规则,比如说:我们需要匹配字符串 'mistyyy' 后面必须是 'good'
举个例子:
reg = /mistyyyygood/ 复制代码
这个时候,'mistyyyy' 后面是 'good' 但是此时,'good' 也被匹配到了,如果我们用 replace 做替换,那么 good 也会被替换掉。
要满足这样的条件。我们可以使用正向匹配
规则如下:
- /expression(?=condition)/ 这表示expression后面必须紧跟 condition 作为条件。
- /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 新加的特性。
规则如下:
- /(?<=condition)expression/ 表示 expression 前必须满足 condition 条件才匹配
- /(?<!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 有相同的作用。那么我们来探寻一下他们的异同点。
相同点:
- 他们都会受 lastIndex 的属性值影响正则开始匹配的效果。即影响从目标字符串的何处开始匹配
- 他们在匹配失败后 lastindex 的属性都会置为 0。
- 他们匹配成功后,lastIndex 都会重置为匹配成功的字符串的下一个字符。
- 修饰符 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,希望我们可以继续努力,将正则表达式真正的掌握。这样,我们就可以写出更加优雅,更加具有可读性的代码了。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
创新者的窘境(全新修订版)
克莱顿•克里斯坦森 / 胡建桥 / 中信出版社 / 2014-1-1 / 48.00元
全球商业领域中,许多企业曾叱咤风云,但面对市场变化及新技术的挑战,最终惨遭淘汰。究其原因,竟然是因为它们精于管理,信奉客户至上等传统商业观念。这就是所有企业如今都正面临的“创新者的窘境”。 在《创新者的窘境》中,管理大师克里斯坦森指出,一些看似很完美的商业动作——对主流客户所需、赢利能力最强的产品进行精准投资和技术研发——最终却很可能毁掉一家优秀的企业。他分析了计算机、汽车、钢铁等多个行业的......一起来看看 《创新者的窘境(全新修订版)》 这本书的介绍吧!