js正则表达式使用详解

正则表达式

正则表达式(Regular Expression),在代码中常简写为 regex、regexp或RE。使用单个字符串来描述、匹配一系列符合某个句法规则的字符串搜索模式。

搜索是可用于文本搜索和文本替换。

语法:

/正则表达式主体/修饰符(可选)

在 javascript 中, 正则表达式通常用于两个字符串方法:search()replace()

search()方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回子串的起始位置。

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

  • search() 方法的使用
const str = 'hello world'
// 正则表达式
console.log(str.search(/ello/)) // 1
console.log(str.search(/lls/)) // -1
// 字符串:字符串参数会转换为正则表达式
console.log(str.search('ello')) // 1
console.log(str.search('lls')) // -1
  • replace() 方法的使用
const str = 'hello world'
// 正则表达式
console.log(str.replace(/ello/, 'ey')) // hey world
console.log(str.replace(/elll/, 'ey')) // hello world
// 字符串:字符串参数会转换为正则表达式
console.log(str.replace('ello', 'ey')) // hey world
console.log(str.replace('elll', 'ey')) // hello world

正则表达式修饰符

修饰符可以在全局搜索中不区分大小写

修饰符描述
i执行对大小写不敏感的匹配
g执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)
m执行多行匹配
const str = 'hElLo world'
console.log(str.replace(/ello/, 'ey')) // hElLo world
console.log(str.replace(/ello/i, 'ey')) // hey world

const str2 = 'hello world\nRegExp'
console.log(str2.replace(/^Reg/, 'reg')) // hello world\nRegExp 
console.log(str2.replace(/^Reg/m, 'reg')) // hello world\nregExp

const str3 = 'hello world hello RegExp'
console.log(str3.replace(/ello/, 'ey')) // hey world hello RegExp
console.log(str3.replace(/ello/g, 'ey')) // hey world hey RegExp

正则表达式模式

方括号

方括号用于查找某个范围内的字符

表达式描述
[abc]查找方括号之间的任何字符
[^abc]查找任何不在方括号之间的字符
[0-9]查找任何从0至9的数字
[a-z]查找任何从小写a到小写z的字符
[A-Z]查找任何从大写A到大写Z的字符
[A-z]查找任何从大写A到小写z的字符

元字符

元字符是拥有特殊含义的字符

元字符描述
.查找单个字符,除了换行和行结束符
\w查找单词字符 [a-zA-Z_0-9]
\W查找非单词字符
\d查找数字
\D查找非数字字符
\s查找空白字符
\S查找非空白字符
\b匹配单词边界
\B匹配非单词边界
\0查找NULL字符
\n查找换行符
\f查找换页符
\r查找回车符
\t查找制表符
\v查找垂直制表符
\xxx查找以八进制数xxx规定的字符
\xdd查找以十六进制数 dd 规定的字符
\uxxxx查找以十六进制数 xxxx 规定的 Unicode 字符

量词

量词描述
n+匹配任何包含至少一个 n 的字符串
n*匹配任何包含零个或多个 n 的字符串
n?匹配任何包含零个或一个 n 的字符串
n{X}匹配包含 X 个 n 的序列的字符串
n{X, Y}匹配包含 X 至 Y 个 n 的序列的字符串
n{X, }匹配包含至少 X 个 n 的序列的字符串
n$匹配任何结尾为 n 的字符串
^n匹配任何开头为 n 的字符串
?=n匹配任何其后紧接指定字符串 n 的字符串
?!n匹配任何其后没有紧接指定字符串 n 的字符串

RegExp 对象方法

方法描述
compile编译正则表达式
exec检索字符串中指定的值,返回找到的值,并确定其位置
test检索字符串中指定的值,返回 true 或 false
const str = 'hello world hello RegExp'
console.log(/ello/.compile(/ello/)) // /ello/
console.log(/ello/.exec(str)) // [ 'ello', index: 1, input: 'hello world hello RegExp', groups: undefined ]
console.log(/ello/.test(str)) // true

test方法的坑

test 方法用于测试字符串参数中是否存在匹配正则表达式模式的字符串。

lastIndex:是当前表达式匹配内容的最后一个字符的后一位,用于规定下一次匹配的起始位置。

当正则表达式使用了全局匹配时,test方法会出现奇怪现象:

let reg = /\w/g

console.log(reg.test('ab')) // true
console.log(reg.test('ab')) // true
console.log(reg.test('ab')) // false
console.log(reg.test('ab')) // true

正常情况,ab符合 test方法,都应该返回 true才对,原因就在于 lastIndex 属性。

我们可以试试每次都运行 test 方法打印出 lastIndex的值:

let reg = /\w/g
console.log(reg.lastIndex) // 0

console.log(reg.test('ab')) // true
console.log(reg.lastIndex) // 1
console.log(reg.test('ab')) // true
console.log(reg.lastIndex) // 2
console.log(reg.test('ab')) // false
console.log(reg.lastIndex) // 0
console.log(reg.test('ab')) // true
console.log(reg.lastIndex) // 1

lastIndex属性是当前表达式匹配内容的最后一个字符的后一位,用于规定下一次匹配的起始位置。

当进入正则表达式全局模式时,每次使用 test 方法都会从 lastIndex 开始,匹配从 lastIndex 开始的子字符串。比如例子中,第二次执行 test 方法时,此时,lastIndex 已经变为2,子字符串为空,所以 reg 不可能匹配上它,由于子字符串匹配失败,test 方法返回 false,并将 lastIndex 属性置为0,重新开始一轮循环。

避免 test 中的坑的方法

  • test方法本身就是用来测试是否存在匹配正则的字符串,不使用全局模式一样可以实现目的,所以第一种方法就是不适用全局模式;
  • 不将正则对象实例存在变量中,每次直接用正则对象实例调用 test方法,不过这种方法对内存有所损耗,理论上不建议。

支持正则表达式的 String 对象的方法

方法描述
search检索与正则表达式相匹配的值
match找到一个或多个正则表达式的匹配
replace替换与正则表达式匹配的子串
split把字符串分割为字符串数组
const str = 'hello world hello RegExp'
console.log(str.search(/ello/)) // 1
console.log(str.search(/Ello/)) // -1

console.log(str.match(/ello/)) // [ 'ello', index: 1, input: 'hello world hello RegExp', groups: undefined ]

console.log(str.replace(/ello/, 'ey')) // hey world hello RegExp
console.log(str.replace(/Ello/, 'ey')) // hello world hello RegExp

console.log(str.split(/ello/)) // [ 'h', ' world h', ' RegExp' ]
console.log(str.split(/ello/, '1')) // [ 'h' ]
console.log(str.split(/ello/, 2)) // [ 'h', ' world h' ]
console.log(str.split(/ello/, 3)) // [ 'h', ' world h', ' RegExp' ]

search 方法

String.prototype.search(reg)

search方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回子串的起始位置。方法返回第一个匹配结果的 index。查找不到则返回 -1

  • search方法不执行全局匹配,它将忽略修饰符 g,并且总是从字符串的开始进行检索,因此,它不会产生类似于 test 方法的问题。
  • 不输入正则表达式则 search方法将会自动将其转为正则表达式。

match 方法

String.prototype.match(reg)

match 方法将检索字符串,以找到一个或多个与 reg 匹配的文本,reg是否具有修饰符 g对结果影响很大。

非全局调用

如果 reg 没有修饰符 g,那 match 方法就只能在字符串中执行一次匹配,如果没有找到任何匹配的文本,将返回 null。否则,它将返回一个数组,其中存放了与它找到的匹配文本有关的信息。

返回的数组的第一个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。

除了常规的数组元素外,返回的数组还含有2个对象属性:

  • index:声明匹配文本的起始字符在字符串的位置
  • input:声明对 stringObject的引用
let str = 'af12131ds22'
console.log(str.match('v')) // null
console.log(str.match('s')) // [ 's', index: 8, input: 'af12131ds22', groups: undefined ]

全局调用:

如果 reg具有修饰符 g,则 match 方法将执行全局检索,找到字符串中的所有匹配子字符串。如果没有找到任何匹配的子串,将返回 null。否则,返回一个数组。数组元素中存放的是字符串中所有匹配到的子串,而且也没有 index 属性或 input 属性。

let str = 'af12131ds22'
console.log(str.match(/[^\w]/g)) // null
console.log(str.match(/\d+/g)) // [ '12131', '22' ]

split 方法

String.prototype.split(reg)

把字符串分割为字符数组

let str = 'af12131ds22'
console.log(str.split('')) // [ 'a', 'f', '1', '2', '1', '3', '1', 'd', 's', '2', '2' ]
console.log(str.split('1')) // [ 'af', '2', '3', 'ds22' ]

一些复杂情况可以使用正则表达式解决

let str = 'af12131ds22'
console.log(str.split(/\d/)) // [ 'af', '', '', '',   '', 'ds', '',   '' ]
console.log(str.split(/\d+/)) // [ 'af', 'ds', '' ]

replace 方法

String.prototype.replace()

replace 方法有三种形态:

  • String.prototype.replace(str, replaceStr)
  • String.prototype.replace(reg, replaceStr)
  • String.prototype.replace(reg, function)
let str = 'af12131ds22'
console.log(str.replace('1', 7)) // af72131ds22
console.log(str.replace(/1/g, 7)) // af72737ds22

str.replace(/1/g, function (...args) {
  console.log(args)
})
/**
 * [ '1', 2, 'af12131ds22' ]
 * [ '1', 4, 'af12131ds22' ]
 * [ '1', 6, 'af12131ds22' ]
 */

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

  • 匹配字符串
  • 正则表达式分组内容,没有分组则没有该参数
  • 匹配项在字符串中的 index
  • 原字符串
// 有分组内容的
let str = 'af12131ds22'
str.replace(/(\d)(\w)/g, function (...args) {
  console.log(args)
})
/**
 * [match, group1, group2, index, origin]
[ '12', '1', '2', 2, 'af12131ds22' ]
[ '13', '1', '3', 4, 'af12131ds22' ]
[ '1d', '1', 'd', 6, 'af12131ds22' ]
[ '22', '2', '2', 9, 'af12131ds22' ]
 */

let str = 'af12131ds22'
str.replace(/(\d)(\w)(\d)/g, function (...args) {
  console.log(args)
})
/**
 * [match, group1, group2, group3, index, origin]
[ '121', '1', '2', '1', 2, 'af12131ds22' ]
 */

后行断言

JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead)。不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。ES2018 引入后行断言

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

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

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

let lookbehead = /\d+(?=%)/.exec('100% of US presidents have been male')
console.log(lookbehead)
let negativeLookbehead = /\d+(?!%)/.exec('that is all 44 of them')
console.log(negativeLookbehead)
/**
 * [
    '100',
    index: 0,
    input: '100% of US presidents have been male',
    groups: undefined
  ]
  [ '44', index: 12, input: 'that is all 44 of them', groups: undefined ]
 */

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

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

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

let lookbehind = /(?<=\$)\d+/.exec('$100 of US presidents have been male')
console.log(lookbehind)
let negativeLookbehind = /(?<!\$)\d+/.exec('that is all &44 of them')
console.log(negativeLookbehind)
/**
 * [
      '100',
      index: 1,
      input: '$100 of US presidents have been male',
      groups: undefined
    ]
    [
      '44',
      index: 13,
      input: 'that is all &44 of them',
      groups: undefined
    ]
 */

上面例子中,“后行断言”的括号之中的部分((?<=\$)),也是不计入返回结果。

下面的例子是使用后行断言进行字符串替换。

let reg = /(?<=\$)foo/g
console.log('$foo %foo foo'.replace(reg, 'bar')) // $bar %foo foo

上面代码中,只有在美元符号后面的 foo才会被替换。

“后行断言”的实现,需要先匹配 /(?<=y)x/x,然后再回到左边,匹配 y 的部分。这种“先右后左”的执行顺序,与所有其他正则操作相反,导致了一些不符合预期的行为。

具名组匹配

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

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 引入了具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。

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>$/
console.log(reg.test('abc!abc')) // true
console.log(reg.test('abc!ab')) // false

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

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

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

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

String.prototype.matchAll()

如果一个正则表达式在字符串里面有多个匹配,现在一般使用 g 修饰符或 y 修饰符,在循环里面逐一取出。

let regex = /t(e)(st(\d?))/g
let string = 'test1test2test3'

let matches = []
let match
while (match = regex.exec(string)) {
  matches.push(match)
}

console.log(matches)
// [
//   ["test1", "e", "st1", "1", index: 0, input: "test1test2test3", groups: undefined],
//   ["test2", "e", "st2", "2", index: 5, input: "test1test2test3", groups: undefined],
//   ["test3", "e", "st3", "3", index: 10, input: "test1test2test3", groups: undefined]
// ]

console.log(reg.exec(str))
// [ 'test1', 'e', 'st1', '1', index: 0, input: 'test1test2test3', groups: undefined ]

上面代码中,while 循环取出每一轮的正则匹配,一共三轮。

ES2020 增加了 String.prototype.matchAll() 方法,可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组。

let reg = /t(e)(st(\d?))/g
let str = 'test1test2test3'
for (const match of str.matchAll(reg)) {
  console.log(match)
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3", groups: undefined]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3", groups: undefined]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3", groups: undefined]

上面代码中,由于 str.matchAll(reg) 返回的是遍历器,所以可以用 for...of循环取出。相对于返回数组,返回遍历器的好处在于,如果匹配结果是一个很大的数组,那么遍历器比较节省资源。

遍历器转为数组是非常简单的,使用 ... 运算符和 Array.from() 方法就可以了

// 转为数组的方法一
[...str.matchAll(reg)]
// 转为数组的方法二
Array.from(str.matchAll(reg))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值