深入 JavaScript 的正则表达式

679 阅读4分钟

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

正则表达式应该是一个十分常用的功能了,不仅是业务逻辑会用到它,甚至在代码编辑器上用正则表达式查找替换的场景也不少,今天就来深入了解一下 JavaScript的正则表达式。

参考资料

本文结构

  • 正则表达式的知识点
  • JavaScript 与正则表达式相关的语言内置 api
  • 正则表达式的使用场景
  • 正则表达式的实现原理

正则表达式的知识点

mdn文档:正则表达式 - JavaScript | MDN (mozilla.org) mdn文档已经讲的很详细了,如果想要全面了解js的正则表达式,请自行查阅文档,如果想觉得阅读完整个 mdn 文档是一个心智负担不小的任务,只想了解其中有意思和有坑的部分,那就可以继续看下去了。

正则表达式是一种能够满足字符串模式匹配任务的特殊语法。js定义一个正则表达式的对象最简单的方法就是将表达式放在一对双斜杠之间,如 /[1-9][0-9]+/

基本内容

正则表达式匹配一个字符有以下几种方式:

  • 普通ASCII字符,如/a/就是匹配字符a
  • 范围内字符
    • 枚举字符:/[abc]/匹配字符a或b或c,使用一对中括号表示
    • 或字符:a|b|c匹配a或b或c,用中竖线|分隔
    • 连字符范围:/[a-zA-Z]/匹配任意大小写字母,a-z表示a到z的范围(包括a和z),在中括号内的连字符要求-左边字符的ascii值不超过-右边字符的ascii值,否则会报错Range out of order in character class
    • 范围取反:/[^\n\r]/匹配非换行符, 特点是在中括号内以^开头
  • 约定模式:\s匹配空白字符(空格、制表符、换页符和换行符),\w匹配一个字母、数字或者下划线,\d匹配一个数字,这些字母都是小写,如果是大写,则表示取反,\S表示非空白字符

正则表达式还有量词匹配有几种case:+匹配一个或多个表达式,*匹配0或多个表达式,?匹配0或1个表达式,{n,m}表示匹配 x 个表达式,x在n到m之间,如 /(zan){1,3}/可以匹配 zanzanzanzanzanzan注意{n,m} 中间不能出现空格,否则会失效。

进阶内容

  • 捕获分组:如果在正则表达式上给某段子串加上圆括号,这就是一个捕获分组,如/(foo)(bar)/有两个分组,第一个是 foo,第二个是 bar,正则表达式不仅可以获取匹配的内容,还可以提取其中的捕获分组
  • 非捕获分组:有时我们使用圆括号是想将某段表达式视为一个整体,并不是想形成一个捕获分组,可以使用(?:expression),比如 /40(?:1|3) (\w+?)\./ 可以匹配 403 forbidden.,第一个捕获分组是 forbidden, 如果换成/40(1|3) (\w+?)\./,则第一个捕获分组是1
  • 先行断言:使用(?=expr)表达式,括号内内容不参与匹配,但必须该内容存在,前面的内容才能被匹配。如/403 (?=\w+?\.)/对于字符串403 forbidden.匹配成功,匹配内容是403,但是对403字符串匹配不成功。先行意味着待匹配内容在断言表达式前面。
  • 后行断言:与先行断言概念相似,使用(?<=expr)表达式,待匹配内容在断言表达式后面。如(?<=hello) world匹配字符串hello world的匹配结果是 world
  • 先行非断言:(?!expr),与先行断言相对,当括号内的表达式不存在,前面的内容才能被匹配
  • 后行非断言:(?<!expr),与后行断言相对,当括号内的表达式不存在,后面的内容才能被匹配

js 与正则表达式有关的内容

RegExp对象

创建一个 RegExp 有两种方法: 第一种是new操作符创建

let re = new RegExp('[a-b]', 'g')

另一种是字面量创建

let re = /ab/g

对于RegExp常用的两个方法是 test(str)exec(str)

test方法测试一个字符串是否能够被正则表达式匹配,返回truefalse

exec方法用于获取一个字符串的匹配内容和捕获分组,返回一个null(如果匹配失败的话)或特殊数组。如/40(?:1|3) (\w+?)\./.exec('403 forbidden.')的返回结果是['403 forbidden.', 'forbidden'],第0项表示匹配内容,第1项表示第一个捕获分组,第n项表示第n个捕获分组

String 对象

String的 match、matchAll、split、replace、search都能使用正则表达式。

str.match(re)

match方法作用于RegExp对象的exec方法类似,区别如下

const str = 'abab'
const re = /a(b)/g
console.log(re.exec(str))// 第一次
console.log(re.exec(str))// 第二次
console.log(re.exec(str))// 第3次
str.match(re)

输出结果如下:

['ab', 'b']
['ab', 'b']
null
['ab', 'ab']

加了g全局匹配标识的正则表达式对象,执行过一次后exec后,下次匹配从上次匹配成功的结束位置(.lastIndex)开始,就是说第一次匹配ab后(返回的数组包括匹配内容和捕获分组),下次从下标为2的位置开始匹配,如果匹配失败则返回null,下次exec从头开始

如果是字符串执行match方法,在正则表达式加了g标识的情况下,直接返回所有匹配内容,不包括捕获分组,相当于多次exec方法的数组第0项的聚合

str.matchAll

作用于match方法相同,但要求正则表达式必须加g标识,且返回的是一个迭代器,可以用 for-in 循环展开,也可以用 .next()访问

replace

用于替换字符串,第二个参数如果是字符串,则代表替换后的新值,里面的 $n表示匹配的第n个捕获分组, $&表示整个匹配内容;如果第二个参数是一个函数,则函数的第一个参数表示匹配内容,第n+1个参数表示第n个捕获分组,返回值就是替换后的新值

'player vs player233'.replace(/(\w+?) vs (\1(233))/g, "for $&, $2 is loser") 
// 结果是'for player vs player233, player233 is loser'

'custom-element-tag'.replace(/(?:^|-)([a-zA-Z])/g, (_, ch) => ch.toUpperCase())
// 结果是'CustomElementTag'

search

和indexOf方法相似,返回第一个匹配内容的下标位置,如果找不到则返回-1,但是indexOf不支持正则表达式。

split

将字符串按正则表达式匹配的内容分隔分组,返回分组数组。

使用场景

在涉及到编译的项目用得比较多,如 vue、react 将模板或jsx编译成 render 函数,babel将es6转es5,驼峰命名风格转换,markdown渲染引擎······涉及到词法分析的分词功能(将将字符流转换成一个个的词法符号),我们也会很容易用到正则表达式。 vuejs/vue-next仓库的packages/compiler-core/src/parse.ts的匹配html标签的tag 名的部分代码如下:

  // Tag open.
  const start = getCursor(context)
  const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
  const tag = match[1]
  const ns = context.options.getNamespace(tag, parent)

  advanceBy(context, match[0].length)
  advanceSpaces(context)

实现原理

正则表达式是可以通过 DFA(确定性的有限状态机)实现的,没精力研究原理了,读者有兴趣自行百度吧,可以给以下参考资料: