谈谈 JavaScript 的正则表达式
一、背景
最近在做 CMS 系统中不同身份登录用户的权限管理,涉及到对 api 路径的识别去判断是否放行。以前对正则表达式都是敬而远之,要用到的话都是直接复制粘贴现成网上的表达式,看也看不太懂,借这次机会熟悉下,不求钻的多深,但求有个整体的认知,满足我目前的简单需求即可。
二、介绍
正则表达式
(Regular Expression)是一种用来匹配字符串的强有力的工具。
各个编程语言对正则表达式的支持标准有所差异,这里只以 JavaScript 为例。
三、用法
本文对 非捕获括号、正向肯定查找、正向否定查找 不做介绍。
1、匹配符
匹配符 | 可以匹配 |
---|---|
Iamstring | Iamstring |
. | 一个任意字符 |
\d | 一个数字 |
\D | 一个非数字 |
[a-zA-Z] | 一个字母 |
[^a-zA-Z] | 一个非字母 |
\w | 一个字母、数字或者下划线 |
[0-9a-zA-Z_] | 同上 |
\s | 一个空格(还包括制表符、换页符和换行符) |
[\u4e00-\u9fa5] | 中文 |
A|B | A 或 B |
[ABC] | A 或 B 或 C |
[A-C] | 同上 |
2、特殊符号
上面的匹配符可以搭配下表的符号,如 /[A-Z]{3}/
:
如果遇到歧义可使用括号 (),如/bo+/
和 /(bo)+/
表达的意思不一样。
特殊符号 | 表示 | 放匹配符的前面 | 放匹配符的后面 | 备注 |
---|---|---|---|---|
{n} | n个字符 | √ | ||
{n,m} | n-m个字符 | √ | ||
* | 0或多个字符 | √ | 等价于{0,} | |
+ | 至少一个字符 | √ | 等价于{1,} | |
? | 0或1个字符 | √ | 等价于{0,1} | |
^ | 表示行的开头 | √ | ||
$ | 表示行的结束 | √ | ||
\b | 表示单词的边界 | √ | √ | |
\b | 表示单词的非边界 | √ | √ | |
如果上述特殊符号是普通字符的话,需要用\
转义,如[A-Z]?{3}
不对而[A-Z]\?{3}
可以正确匹配上 "D???"。
3、贪婪模式和懒惰模式
(1)如何设置?
JavaScript 的正则表达式默认为贪婪模式
(匹配尽量多的字符)。
但如果在 *、 +、? 或 {} 的后面出现?
,将会变为非贪婪的懒惰模式
(匹配尽量少的字符)。
例如,对 "123abc" 应用 /\d+/ 将会返回 "123",如果使用 /\d+?/,那么就只会匹配到 "1"。
(2)有何区别?
上图里有个术语叫回溯
,会影响到正则匹配字符串的性能,在平时写正则的时候需要注意,详细请看下文的三、性能问题
注:java 的正则里还有一种模式叫独占模式
,貌似 JS 里没有(待求证)。
4、JavaScript 里使用
(1)创建和 function
// *** 创建正则表达式 *** // 方法一:/正则表达式主体/修饰符(可选) var reg1 = /\d/; // 若 RegExp 是全局模式 // var reg1 = /\d/g; // 方法二:new RegExp('正则表达式') // 注意:不用添加/***/,且由于是字符串,所以需要考虑转义! // var reg2 = new RegExp("\\d"); // 若 RegExp 是全局模式 // var reg2 = new RegExp("\\d",'g'); var string = '你好 123 123 js' // *** RegExp 的方法 - test() exec() *** console.log(reg1.test(string)); // true or false console.log(reg1.exec(string)); // [ '1', index: 3, input: '你好 123 123 js' ] console.log(reg1.exec(string)); // [ '1', index: 3, input: '你好 123 123 js' ] (跟上面一样) // 若 RegExp 是全局模式 console.log(reg1.exec(string)); // [ '1', index: 3, input: '你好 123 123 js' ] console.log(reg1.exec(string)); // [ '2', index: 4, input: '你好 123 123 js' ] console.log(reg1.exec(string)); // [ '3', index: 5, input: '你好 123 123 js' ] console.log(reg1.exec(string)); // [ '1', index: 7, input: '你好 123 123 js' ] console.log(reg1.exec(string)); // [ '2', index: 8, input: '你好 123 123 js' ] console.log(reg1.exec(string)); // [ '3', index: 9, input: '你好 123 123 js' ] console.log(reg1.exec(string)); // null console.log(reg1.exec(string)); // [[ '1', index: 3, input: '你好 123 123 js' ] // *** String 的方法 - search() match() replace() split() *** console.log(string.search(reg1)); // 返回第一个匹配的下标,没有即是 -1 console.log(string.match(reg1)); // [ '1', index: 3, input: '你好 123 123 js' ] (跟RegExp.exec()一样) // 若 RegExp 是全局模式 console.log(string.match(reg1)); // [ '1', '2', '3', '1', '2', '3' ] console.log(string.replace(reg1,'替换')); // "你好 替换23 123 js" // 若 RegExp 是全局模式 console.log(string.replace(reg1,'$2-$1')); // "你好 替换替换替换 替换替换替换 js" console.log(string.split(reg1)); // [ '你好 ', '', '', ' ', '', '', ' js' ]
除了g
表示全局模式,还有i
表示忽略大小写,m
表示执行多行匹配。
(2)分组
用捕获括号()
可以分组,受影响的是 match()、split()、replace() 方法。
var string = '010-12345' // 无() var reg1 = /^\d{3}-\d{3,8}$/; // 有() var reg2 = /^(\d{3})-(\d{3,8})$/; // match() console.log(string.match(reg1)); // [ '010-12345', index: 0, input: '010-12345' ] console.log(string.match(reg2)); // [ '010-12345', '010', '12345', index: 0, input: '010-12345' ] // split() console.log(string.split(reg1)); // [ '', '' ] console.log(string.split(reg2)); // [ '', '010', '12345', '' ] // replace() - 注意 $1、$2 的含义 console.log(string.replace(reg1,'替换')); console.log(string.replace(reg2,'$2-$1')); // 12345-010
三、性能问题
首先,实现正则表达式引擎
有两种方式:DFA
自动机(Deterministic Final Automata 确定型有穷自动机)和 NFA
自动机(Non deterministic Finite Automaton 不确定型有穷自动机)。
对于这两种自动机,他们有各自的区别,这里并不打算深入将它们的原理。简单地说,DFA 自动机的时间复杂度是线性的,更加简单稳定,但是功能有限。而 NFA 的时间复杂度比较不稳定,有时候很好,有时候不怎么好,好不好取决于你写的正则表达式,但是胜在功能更加强大。
JS 与大多数主流语言的正则引擎选用的都是 NFA。
但需要注意的是,这种正则表达式引擎在进行字符匹配时会发生回溯
(backtracking)。而一旦发生回溯,那其消耗的时间就会变得很长,有可能是几分钟,也有可能是几个小时,时间长短取决于回溯的次数和复杂度。
所以,我们更要在平时写正则表达式的时候,更加注重性能,避免回溯机制带来的隐患。我们可以用 https://regex101.com/
来测试下match 的时间如何。