2
头图

作者:yrainly

公众号:前端开心果

正则表达式进阶内容

👉正则表达式基础介绍内容请戳这里哦

先补充下之前介绍过的replace()方法的使用。

replace()方法详解

replace()方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。

replace 方法有三种形态:

  • String.prototype.replace(str, replaceStr)
  • String.prototype.replace(reg, replaceStr)
  • String.prototype.replace(reg, function)

使用示例1:

const str = 'Hello Kaixinguo'
// 正则表达式
str.replace(/ello/, 'ey') // hey Kaixinguo
str.replace(/elll/, 'ey') // Hello Kaixinguo
// 字符串:字符串参数会转换为正则表达式
str.replace('ello', 'ey') // hey Kaixinguo
str.replace('elll', 'ey') // Hello Kaixinguo
​
str.replace(/o/g, function (...args) {
  console.log(args)
})
// ['o', 4, 'Hello Kaixinguo']
// ['o', 14, 'Hello Kaixinguo']

replace 接受函数参数时,有四个参数:

  • 匹配字符串
  • 正则表达式分组内容,没有分组则没有该参数
  • 匹配项在字符串中的 index
  • 原字符串

上面的使用示例1,replace接收函数参数时没有分组,所以打印出来的只有3个参数。

下面示例演示正则表达式使用了分组的情况。

使用示例2:

const str = 'Hello Kaixinguo,'
​
str.replace(/(\w{2})/g, function (...args) {
  console.log(args)
})
// 有几个分组就有几个group
/**
 * [match, group1, index, origin]
 * ['He', 'He', 0, 'Hello Kaixinguo,']
  ['ll', 'll', 2, 'Hello Kaixinguo,']
  ['Ka', 'Ka', 6, 'Hello Kaixinguo,']
  ['ix', 'ix', 8, 'Hello Kaixinguo,']
  ['gu', 'gu', 12, 'Hello Kaixinguo,']
 * **/

后行断言

支持先行断言(lookahead),先行否定断言(negative lookahead),后行断言(lookbehind)和后行否定断言(negative lookbehind)。ES2018 引入后行断言

表达式描述
?=n匹配任何其后紧接指定字符串 n 的字符串
?!n匹配任何其后没有紧接指定字符串 n 的字符串

先行断言

x只有在 y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/

先行否定断言

x只有不在 y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/

使用示例:

// 先行断言
let behead = /\d+(?=%)/.exec('关注我的人中奖率甚至都达到了100%')
console.log(behead)
// ['100', index: 14, input: '关注我的人中奖率甚至都达到了100%', groups: undefined]
​
// 先行否定断言
let notBehead = /\d+(?!%)/.exec('有14个人关注了我,他们的中奖率能到100%')
console.log(notBehead)
// ['14', index: 1, input: '有14个人关注了我,他们的中奖率能到100%', groups: undefined]

上面两个字符串,如果互换正则表达式,就不会得到相同的结果。另外,还可以看到,“先行断言”括号之中的部分((?=%)),是不计入返回结果的。

后行断言

正好与“先行断言”相反,x只有在 y 后面才匹配,必须写成 /(?<=y)x/。比如,只匹配美元符号后面的数字,要写成/(?<=$)\d+/

后行否定断言

则与“先行否定断言”相反,x 只有不在y后面才匹配,必须写成 /(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成 /(?<!$)\d+/

使用示例:

// 后行断言
let lookbehead = /(?<=$)\d+/.exec('关注我的人都会中$100万')
console.log(lookbehead)
 // ['100', index: 9, input: '关注我的人都会中$100万', groups: undefined]
​
// 后行否定断言
let negativeLookbehead = /(?!=$)\d+/.exec('有14个人关注了我,他们都中了$100万') 
console.log(negativeLookbehead)
// ['14', index: 1, input: '有14个人关注了我,他们都中了$100万', groups: undefined]

具名组匹配

正则表达式使用圆括号进行组匹配

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/

上面代码中,正则表达式里面有三组圆括号。使用 exec 方法,就可以将这三组匹配结果提取出来。

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/
const matchObj = RE_DATE.exec('1997-11-26')
console.log(matchObj)
// [ '1997-11-26', '1997', '11', '26', index: 0, input: '1997-11-26', groups: undefined ]
const year = matchObj[1] // 1997
const month = matchObj[2] // 11
const day = matchObj[3] // 26

组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号(比如 matchObj[1])引用,要是组的顺序变了,引用的时候就必须修改序号。

ES2018 引入了具名组匹配,允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。

const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
​
const matchObj = RE_DATE.exec('1997-11-26')
console.log(matchObj)
// [ '1997-11-26', '1997', '11', '26', index: 0, input: '1997-11-26', groups: [Object: null prototype] { year: '1997', month: '11', day: '26' } ]
​
const year = matchObj.groups.year // 1997
const month = matchObj.groups.month // 11
const day = matchObj.groups.day // 26

上面代码中,“具名组匹配”在圆括号内部,模式的头部添加“问号+尖括号+组名”(?<year>),然后就可以在 exec 方法返回结果的 groups 属性上引用该组名。同时,数字序号 (matchObj[1]) 依然有效。

具名组匹配等于为每一组匹配加上了ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。

如果具名组没有匹配,那么对应的 groups 对象属性会是 undefined

const reg = /^(?<as>a+)?$/
const matchObj = reg.exec('')
console.log(matchObj)
console.log(matchObj.groups.as) // undefined
console.log('as' in matchObj.groups) // true
/**
 * [
  '',
  undefined,
  index: 0,
  input: '',
  groups: [Object: null prototype] { as: undefined }
]
 */

上面代码中,具名组 as 没有找到匹配,那么 matchObj.groups.as 属性值就是 undefined,并且 as 这个键名在 groups 是始终存在的。

解构赋值和替换

有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。

let { groups: { one, two } } = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar')
console.log(one, two) // foo bar

字符串替换时,使用 $<组名>引用具名组。

let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
console.log('2006-06-07'.replace(reg, '$<day>/$<month>/$<year>')) // 07/06/2006

上面代码中,replace 方法的第二个参数是一个字符串,而不是正则表达式。

replace 方法的第二个参数也可以是函数,该函数的参数序列如下。

let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
let str = '2015-01-02'.replace(reg, (
   matched, // 整个匹配结果 2015-01-02
   capture1, // 第一个组匹配 2015
   capture2, // 第二个组匹配 01
   capture3, // 第三个组匹配 02
   position, // 匹配开始的位置 0
   S, // 原字符串 2015-01-02
   groups // 具名组构成的一个对象 {year, month, day}
 ) => {
  let { day, month, year } = groups
  return `${day}/${month}/${year}`
})
console.log(str) // 02/01/2015

具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。

let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
​
let str1 = '2015-01-02'.replace(reg, (...args) => {
  console.log(args)
  let { day, month, year } = args[6]
  return `${day}/${month}/${year}`
})
console.log(str1)
/**
 * [
  '2015-01-02',
  '2015',
  '01',
  '02',
  0,
  '2015-01-02',
  [Object: null prototype] { year: '2015', month: '01', day: '02' }
]
 */

引用

如果要在正则表达式内部引用某个“具名组匹配”,可以使用 \k<组名>的写法。

const reg = /^(?<word>[a-z]+)!\k<word>$/
reg.test('abc!abc') // true
reg.test('abc!ab') // false

数字引用(\1)依然有效。

const reg = /^(?<word>[a-z]+)!\1$/
reg.test('abc!abc') // true
reg.test('abc!ab') // false

这两种引用方法还可以同时使用。

const reg = /^(?<word>[a-z]+)!\k<word>!\1$/
reg.test('abc!abc!abc') // true
reg.test('abc!ab!ab') // false

最后,分享几个可以在线测试正则表达式的地址

结语

❤️ 🧡 💛大家喜欢我写的文章的话,欢迎大家点点关注、点赞、收藏和转载!!

欢迎关注公众号前端开心果 🔥我会持续更新前端相关的内容文章哦。


风如也
202 声望11 粉丝

分享努力写前端代码的生活