正则表达式

JS 中,正则表达式与其他对象类型相似,有两种方式创建正则表达式:

  • 正则表达式字面量
  • 创建 RegExp 对象实例

在使用字面量的时候正则表达式用 / 分割:

const reg = /text/

如果创建 RegExp 对象实例则可以省略掉:

const reg = new RegExp('test')

在实际开发中推荐使用字面量的方式创建正则表达式,当需要在运行时动态创建字符串来构建正则表达式的时候使用构造函数的方式

修饰符

正则表达式中有 5 个修饰符:

  • i
    • 对大小写不敏感,例如/test/i,可以匹配 testTest 等等
  • g
    • 查找所有匹配项,不会再找到第一个匹配项之后就停止查找
  • m
    • 允许多行匹配,对获取 textarea 元素 value 值很有用
  • y
    • 开启粘连匹配,正则表达式执行粘连匹配时试图从最后一个匹配的位置开始
  • u
    • 允许使用 Unicode 点转义字符(\u{...}

在定义字面量的时候修饰符写在后面:/test/ig

在使用构造函数的时候将修饰符作为第二个参数传给构造函数:new RegExp('test', 'ig')

匹配字符集

通常我们不需要像 /test/ig 这样匹配字符串,只想匹配一组有限的字符集中的字符,所以可以将我们希望匹配的字符集放在 []

[abc] 表示我们希望匹配 a、b、c 中任意一个字符

如果想反过来匹配除了 a、b、c 外其他字符,可以这样:[^abc]

如果想匹配的字符太多,可以使用 -[a-m] 匹配 am 中所有字符

如果除了 a-m 之间的字符,还想匹配 z,可以加个逗号:[a-m,z] 就可以了

转义字符

我们知道有很多操作符,比如 $^ 等等,如果使用正则表达式匹配这些字符而又不会被当成操作符呢?

使用转义字符就可以了:\$ 匹配一个 $ 字符

起止符号

^test 匹配的是 test 出现在字符串的开头

$ 表示字符串的结束:test$ 匹配的是 test 出现在字符串的末尾

重复出现

假如你想匹配任意数量的相同字符可以试试以下几种方式:

  • ?
    • (指定字符可以出现 0 或 1 次)在字符串后面加 ?,t?est 可以同时匹配 testest
  • +
    • (指定字符必须出现 1 或多次),/t+est/ 可匹配 test、ttest、tttest
  • *
    • (指定字符出现 0 次或 1 次或多次),如 t*est 可以匹配 test、ttest、est
  • {}
    • 使用{}指定重复次数,t{4}est 就是匹配 ttttest
    • 还可以指定重复次数范围,t{4,10}est 匹配的是 4 到 10 个连续的 t 接上 est
    • 还可以指定开放区间,t{4,}est 可以指定 4 或更多个连续的 t 接上 est

这些运算符都可以是贪婪或者非贪婪的,默认是贪婪模式,可以匹配所有可能的字符,在运算符后面加?,比如 a+? 会使得运算为非贪婪模式,只进行最小限度的匹配

比如一个字符串 aaa/a+?/ 只会匹配一个 a,而 /a+/ 会匹配三个 a

预定义字符集

预定义元字符匹配的字符串
\t水平制表符
[\b]退格
\v垂直制表符
\f换页符
\r回车符
\n换行符
\cA:\cZ控制字符
\u0000:\uFFFF十六进制 Unicode
\x00:\xFF十六进制 ASCII
.匹配除换行字符(\n\r\u2028\u2029)之外的任意字符
\d匹配任意十进制数字,等价于[0-9]
\D匹配除了十进制数字外的任意字符,等价于[^0-9]
\w匹配任何字母,数字和下划线,等价于[A-Za-z0-9_]
\W匹配除了字母,数字和下划线之外的字符,等价于[^A-Za-z0-9_]
\s匹配任意空白字符(包括空格,制表符,换页符等等)
\S匹配除了空白字符外的任意字符
\b匹配一个单词边界,即字与空格间的位置
\B匹配非单词边界(单词内部)

分组

如果想对一组术语使用操作符,可以用圆括号进行分组,这与数学表达式类似,/(ab)+/ 匹配多个连续的 ab 字符串

|表示或,/a|b/ 可以匹配 a 或者 b/(ab)+|(cd)+/ 可以匹配一个或多个 abcd

反向引用

例如正则表达式 /^([dtn])a\1/ 匹配的内容是以字母 d,t,n 开头,其后连续字母 a,在连接第一个分组中捕获的内容

一个非常重要的一点是:上面这个匹配规则并不与 /[dtn]a[dtn]/ 相同,因为 a 后面连接的字母并不是任意的 b,t,n 而必须是与第一个匹配结果完全相同的才行,所以 \1 的结果只有在运行的时候才能确定

在匹配标签元素的时候,反向引用是很有用的:

/<(\w+)>(.+)<\/\1>/ 这个正则表达式可以匹配像 <h1>123</h1> 这样简单的元素,如果没有反向引用,也许就无法匹配了,因为根本没办法知道与起始标记相匹配的结束标记是什么

正则表达式的编译

不管是通过字面量还是构造函数创建的正则表达式,都可以使用 test 方法来编译:

const reg1 = /test/gi
const reg2 = new RegExp('test', 'ig')
const contentText = 'dtaysdasdteststest'

console.log(reg1.test(contentText))
console.log(reg2.test(contentText))

下面来举个根据标签名查找元素的例子:

<div class="div1">123</div>
<div class="div2">456</div>
<span class="span1">789</span>
function findClassByTagName(className, tagName) {
  const eleArr = document.getElementsByTagName(tagName || '*')
  const regex = new RegExp(`(^|\\s)${className}(\\s|$)`)
  const results = []

  for (let i = 0, ele; (ele = eleArr[i++]); ) {
    if (regex.test(ele.className)) results.push(ele)
  }

  return results
}

捕获匹配片段

举个例子:

<div id="div1" style="transform:translateY(15px);"></div>
function getTranslateY(ele) {
  const transformValue = ele.style.transform

  if (transformValue) {
    const match = transformValue.match(/translateY\(([^\)]+)\)/)
    return match ? match[1] : ''
  }

  return ''
}

const div1 = document.getElementById('div1')

console.log(getTranslateY(div1)) // 15px

在正则表达式中使用圆括号定义捕获,使用 match 方法匹配时,使用局部正则表达式可以返回一个数组,数组中包含了全部匹配内容以及操作中的全部捕获结果

使用全局表达式进行匹配

match 方法中使用局部表达式是可以正确的返回捕获结果的,但如果使用 g 修饰符,那么返回的就不仅是第一个匹配结果,而是全部的匹配结果,但是不会返回捕获的结果

可以使用 exec 方法,可以对一个正则表达式多次调用 exec 方法,每次调用可以返回下一个匹配的结果

const html = '<div class="test"><b>hello</b><i>world</i></div>'
const tag = /<(\/?)(\w+)([^>]*?)>/g
let match,
  num = 0

while ((match = tag.exec(html)) !== null) {
  console.log(match.length)
  num++
}

console.log(num)

首先我们来看一下这里的正则表达式:/<(\/?)(\w+)([^>]*?)>/g

(\/?) 是匹配 0 到 1 个 /(\w+) 匹配一到无限个任意字母数字和下划线,([^>]*?) 匹配的是除了 > 之外的其他字符,比如在这里就匹配到了:class="test",其实就是匹配标签中的属性

这样一共调用了 6 次 exec 方法,因为一共匹配到了六个标签,因为我们使用了 3 对圆括号,所以每次调用 exec 返回的数组长度为 4,比如第一次调用 exec 返回的数组是:

const a = ['<div class="test">', '', 'div', ' class="test"']

第一个元素是匹配到的整个字符串,后面三个才是匹捕获的字符串

捕获的引用

可以使用反向引用来匹配 HTML 标记内容

const html = '<b class="hello">hello</b><i>world</i>'
const pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g
const match = pattern.exec(html)

console.log(match)

这里 \1 指向的是表达式中的 第一个捕获 ,在本例中捕获到的是标记的名称,使用第一个捕获的内容匹配对应的结束标记,但这种先给发是有缺点的,当标签中又嵌套了一个相同的标签,那就会匹配到错误的结果了

我们还可以使用字符串的 replace 方法对替代字符串内获取捕获,而不是使用反向引用

console.log('fontFamily'.replace(/([A-Z])/g, '-$1').toLowerCase()) // font-family

在这段代码中,第一个捕获的值(大写字母 F)通过替代字符($1)进行引用,这种方式可以实现在不知道替代值时定义替换规则,直到匹配运行之前还不知道需要替代的值

未捕获分组

在正则表达式中圆括号不仅表示捕获,也表示分组,但是当一个分组过多的正则表达式就会产生很多不必要的捕获

假如有下面一个正则表达式:

const pattern = /((well-)+)play/

在这里,前缀 well- 可以在 play 前面出现一次或者多次,并且捕获整个前缀,这个表达式需要两对圆括号,但显然我们只需要捕获一个就好了,于是可以改成:

const pattern = /((?:well-)+)play/

这样写只有外层圆括号会创建捕获,内层圆括号变成一个被动子表达式

利用函数进行替换

正则表达式同样也可以应用在 replace 方法中:

'dsdadadg'.replace(/[a-z]/g, 'x')

返回的结果是 xxxxxxxg,当然,这绝不是 replace 最终重要的特性,而且支持替换函数作为参数,当第二个参数为函数的时候,对每一个匹配到的值都会调用一遍

  • 全文匹配
  • 匹配时的捕获
  • 在原始字符串匹配的索引
  • 源字符串
  • 从函数返回的值作为替换值

这在运行的时候提供了巨大的余地来判断应该替换的字符串,包括匹配的信息,比如将 - 连接的单词替换为驼峰式字符串:

const upperLetter = (all, letter) => letter.toUpperCase()

console.log('upper-letter'.replace(/-(\w)/g, upperLetter)) // upperLetter

正则表达式常见应用

匹配换行

const html = '<b>hello</b>\n<i>world</i>'
console.log(/.*/.exec(html)[0]) // <b>hello</b>
console.log(/[\S\s]*/.exec(html)[0]) // <b>hello</b>\n<i>world</i>
console.log(/(?:.|\s)/.exec(html)[0]) // <b>hello</b>\n<i>world</i>

这里我们使用了三种方法,第一种方法匹配失败,后两种成功,/(?:.|\s)/ 使用 . 匹配除换行外所有字符,通过 \s 匹配所有字符包括换行,集合的结果是所有字符,当然 /[\S\s]*/ 因为简洁高效被认为是最优解

匹配 Unicode 字符

想要匹配 Unicode 字符很简单:

const matchAll = /[\w\u0080-\uFFFF_-]+/

除了使用 \w 匹配正常的字符之外,还要使用 \u0080-\uFFFF 匹配 \u0080 以上的全部字符