JS正则表达式的核心基础

209 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

介绍

正则表达式 (Regular Expression) 是用于匹配字符串中字符组合的模式。在 JavaScript 中,正则表达式也是对象。这些模式被用于 RegExp 的 exec 和 test方法,以及 String 的 matchmatchAllreplacesearch 和 split 方法。

为什么使用正式表达式?下面用一个示例说明:

问题:找出 aaa11bbb22ccc33ddd44 字符串中的所有数字并添加到数组中,应该得到 ['11', '22', '33', '44']

不使用正则表达式的情况可能是像下面这样处理的:

    const str = 'aaa11bbb22ccc33ddd44'

    function getNumber(val) {
      let arr = []
      let tempNumber = ''

      for(let i = 0; i < val.length; i++) {
        if(!isNaN(parseFloat(val[i]))) {
          // console.log(val[i])
          tempNumber += val[i]
        } else {
          if(tempNumber) {
            arr.push(tempNumber)
          }
          tempNumber = ''
        }
      }
      // 兼容 字符串最后面是数字的情况
      if(tempNumber) arr.push(tempNumber)
      return arr
    }
    
    console.log(getNumber(str)) // ['11', '22', '33', '44']

用正则表达式来解决这个问题,代码则是下面这样的:

const reg = /\d+/g
const arr = str.match(reg)
console.log(arr) // ['11', '22', '33', '44']

可以看到只需要几行代码就能够得到想要的结果,使用正则表达式解决这个问题的话代码就很简洁。

日常开发中,正则表达式做的工作主要是:对字符串的 查找 替换 验证 分割等。

本文主要写了以下重点内容:

  • 正则的创建
  • 正则的匹配方式
  • 元字符
  • 字符集合
  • 边界
  • 分组
  • 反向引用
  • 匹配模式
  • 命名分组

创建正则

字面量方式

格式:/ [规则] / [修饰符],用两条/ 创建正则,中间定义规则,如 \d+,结尾可以接一个修饰符,如 g 表示全局匹配

例如:

const reg = /\d+/g

构造函数方式

格式:new RegExp([规则], [修饰符])

例如:

const reg = new RegExp('\\d+', 'g')

注意:RegExp 创建正则表达式时,由于规则参数放在字符串中,所以涉及到带 \ 的规则时,都要在前面多加一个 \ 来进行转义。

正则的匹配方式

正则对象的方法

test

test() 方法执行一个检索,用来查看正则表达式与指定的字符串是否匹配。返回 true 或 false

const str = '11abc11abc11'
const reg1 = new RegExp('abc')

console.log(reg1.test(str)) // true

如果正则表达式设置全局匹配 g 修饰符,每一次调用都会改变正则对象的 lastIndex 属性。下次再执行 test() 时,将会从 lastIndex 属性值处开始查找,自动忽略掉之前匹配过的。也就是说 lastIndex 会记录匹配的位置。

const str = '11abc11abc11'
const reg2 = new RegExp('abc', 'g')

console.log(reg2.test(str)) // true
console.log(reg2.lastIndex) // 5
console.log(reg2.test(str)) // true
console.log(reg2.lastIndex) // 10
console.log(reg2.test(str)) // false
console.log(reg2.lastIndex) // 0

exec

exec()  方法在一个指定字符串中执行一个搜索匹配。返回一个结果数组或null。

const str = '11abc11abc11'
const reg1 = new RegExp('abc')

console.log(reg1.exec(str))
console.log(reg1.exec(str))
console.log(reg1.exec(str))

不进行全局匹配时,每次返回的结果都一样,如下:

捕获.PNG

使用全局匹配时:

const str = '11abc11abc11'
const reg2 = new RegExp('abc', 'g')

console.log(reg2.exec(str))
console.log(reg2.exec(str))
console.log(reg2.lastIndex) // 10
console.log(reg2.exec(str))

捕获.PNG

匹配成功时,exec() 方法返回一个数组,并更新正则表达式对象的 lastIndex 属性。由上图可知,第三次匹配失败返回了 null,因为字符串中只有两个 abc 字符。全局匹配时,每次调用 exec() 都是基于当前的 lastIndex,第三次执行时从下标为 10 的地方开始查找,后面没有符合规则的字符了。匹配失败时,也会将 lastIndex 的值重置为 0 。

字符串的方法

方法用途
spilt切割字符串
search查找
match匹配
replace查找替换

spilt

按照数字切换字符串返回一个数组:

const str = 'abc11de11f'
const reg = /\d+/
const res = str.split(reg)
console.log(res) // ['abc', 'de', 'f']

search

查找符合规则的元素,返回第一个匹配元素的索引值,会忽略全局属性。如果没有符合规则的返回-1

const str = 'abc11de11f'
const reg = /\d+/
const res = str.search(reg)
console.log(res) // 3
console.log(str.search(/c/)) // 2

match

匹配符合正则的元素,放到数组中并返回。

const str = 'abc11de11f'
const reg = /\d+/
const res = str.match(reg)
console.log(res) // ['11', index: 3, input: 'abc11de11f', groups: undefined]

如果不是全局匹配的话,返回的数组第一项为匹配到的元素,还会有一些额外附加的数据,有点像 exec() 返回的数据。

全局匹配的情况下,返回一个正常的包含所有匹配项的数组,如下:

const str = 'abc11de11f'
const reg = /\d+/g
const res = str.match(reg)
console.log(res) // ['11', '11']

replace

查找符合规则的元素,替换成其他的内容,再返回处理后的字符串。

const str = 'abc11de11f'
const reg = /\d+/g
const res = str.replace(reg, '*')
console.log(res) // abc*de*f

元字符

元字符:指有特殊含义的非字母字符,如上文经常使用的 \d

大致可以分为以下几类:

  • 字符相关
  • 数量相关
  • 位置相关
  • 括号相关

字符相关

元字符匹配元素
\w数字、字母、下划线
\W非数字、字母、下划线
\d数字
\D非数字
\s空格
\S非空格
.\n\r\u2028\u2029

举例:

const reg = /\w+/g
console.log(reg.test('aa111_1')) // true
console.log(reg.test('aa111_1~')) // false 包含波浪线
const reg = /\W+/g
console.log(reg.test('aa111_1')) // false
console.log(reg.test('aa111_1~')) // true
const reg = /./g
const str1 = `
` // 包含回车
const str2 = '12ddd'
console.log(reg.test(str1)) // false
console.log(reg.test(str2)) // true
const reg = /a.c/g
const str = 'abc'
console.log(reg.test(str)) // true

数量相关

元字符匹配元素
{}设置匹配元素的次数
?匹配0或者1次
+匹配1到无限多次
*匹配0到无限多次

{} 元字符有三种写法:

  • {3} 匹配固定3次
  • {3,5} 匹配3到5次
  • {1,} 匹配1到无限次
const reg = /ab{1,}/g
const str = '111abbbc'
console.log(reg.test(str)) // true
  • ? 可以理解为 {0,1}
  • + 可以理解为 {1,}
  • * 可以理解为 {0,}
// 匹配数字的正则
const reg = /\d?/
const reg1 = /\d+/
const reg2 = /\d*/
const reg3 = /\d{3,5}/

位置相关

元字符匹配元素
^字符串开始位置
$字符串结束位置
\b边界符,匹配有边界的。边界是指非\w的,也就是非数字、字母、下划线的都属于边界
\B匹配没有边界的

举例:

匹配到第一个字符,将第一个字符变成 *

const reg = /^\w/
const str = '12345'
console.log(str.replace(reg, '*')) // *2345

如果只有 ^ 符号,后面不接 \w,那么就会匹配第一个字符前的位置,如下:

const reg = /^/
const str = '12345'
console.log(str.replace(reg, '*')) // *12345

$ 也同理:

const reg = /\w$/  // 匹配最后一个字符
const reg = /$/  // 匹配最后一个字符后面的位置

下面字符串有两个 aa,要匹配到边界的 aa,而不是所有的aa

const reg = /aa/g
const str = '123aa12 aa bb'
console.log(str.match(reg)) // ['aa', 'aa'] 全都匹配到了

const reg2 = /\baa\b/g
console.log(str.match(reg2)) // ['aa'] 只匹配左右都有边界的

匹配左边无边界,右边有边界,长度为2的字符:

const reg = /\B\w{2}\b/g
const str = '123aa12 aa bbb'
console.log(str.match(reg)) // ['12', 'bb']

由上面的例子可以知道,第一个字符的左侧和最后一个字符的右侧也都属于边界

括号相关

元字符作用
{}上文说过的表示匹配次数
()使用()包裹的为子表达式。分组、提取值、替换,反向引用
[]字符集合

提取值

  • 举例:使用 () 提取值:

从字符串中提取出日期:

const str = '2060-12-10'
const reg = /\d{4}-\d{2}-\d{2}/
console.log(str.match(reg)) // ['2060-12-10', index: 0, input: '2060-12-10', groups: undefined]

不适用()元字符的话,提取出来的是整个字符串,加上()能够得到预期结果:

const str = '2060-12-10'
const reg = /(\d{4})-(\d{2})-(\d{2})/
console.log(str.match(reg)) // ['2060-12-10', '2060', '12', '10', index: 0, input: '2060-12-10', groups: undefined]

得到的结果是:['2060-12-10', '2060', '12', '10', index: 0, input: '2060-12-10', groups: undefined],可以通过数组下标找到这些值,也可以像下面这样:

console.log(RegExp.$1) // 2060
console.log(RegExp.$2) // 12
console.log(RegExp.$3) // 10

将匹配到的子表达式保存在了RegExp对象中,RegExp.$1表示保存的第一个子表达式,以此类推。

把年月日换成年/月/日格式显示 :

console.log(str.replace(reg, '$1/$2/$3')) // 2060/12/10

反向引用

  • 反向引用可以调用捕获到的值,举例:

假设一个场景:css样式命名时,可以使用footer-box-item或者footer-box-item,但是-_ 不能同时出现,比如 footer-box_item 这种写法是不允许的,下面用正则来匹配正确的格式:

const css = 'footer-box_item'
const reg = /\w+(_|-)\w+(\1)\w+/
console.log(reg.test(css)) // false
const css = 'footer-box-item'
const reg = /\w+(_|-)\w+(\1)\w+/
console.log(reg.test(css)) // true

(_|-) 表示 第一个字符的位置可以是 _ 或者 -,第二个字符的位置使用了 (\1),表示要匹配到和第一个一样的元素,所以就限制了在这个字符串中只能出现一种符号。

\1 指向 RegExp.$1\2 指向 RegExp.$2,以此类推。

字符集合 []

使用 [] 表示一段字符的集合。

  • 如:AaBb [a-z] [0-9]
  • 字符集合 [] 中的 ^ 表示非,取反的意思。而不是上文中说的匹配开头位置。[^0-9] 表示匹配非数字。
  • 用字符集合[]表示预设的元字符:
    • [0-9] 等价于 \d
    • [0-9A-Za-z_] 等价于 \w

一段内容在 ASCII码 中是连续的,才可以使用 - 进行连接表示一个集合。

贪婪匹配与惰性匹配

  • 贪婪匹配:尽可能多的匹配,默认
  • 惰性匹配:尽可能少的匹配,需要使用 ? 元字符将正则改为惰性匹配

例如:/\d{2,4}/在条件允许的情况下,优先匹配4个数字。而 /\d{2,4}?/在条件允许的情况下,优先匹配2个数字。

匹配模式

匹配模式含义
g全局匹配
i忽略大小写
m多行模式
s. 元字符可以匹配到换行
u匹配unicode编码
y粘性模式:匹配的字符需要是连续的
  • 举例:多行模式
const str = `123
456
789
`
const reg = /^\w/g
console.log(str.replace(reg, '*')) // false

捕获.PNG

如图:只把第一行的第一个字符替换成了 *,使用 m 模式后,可以将三行的首个字符全部替换成 *

const reg = /^\w/gm

捕获.PNG

  • 举例:匹配 unicode 编码
const str = 'a'
const reg = /\u{61}/gu
console.log(reg.test(str)) // true

a 的 unicode 编码是 61

命名分组

() 进行分组时,可以将每组命名,方便调用。

const str = '2060-12-10'
const reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
console.log(str.match(reg))

捕获.PNG

groups中可以找到我们的命名和对应的值。