JS正则表达式完整教程(略长)

正则表达式用于1.匹配字符,或者2.匹配位置

1.字符匹配攻略

  • 横向与纵向匹配
    • {m,n} 匹配m到n长度的字符
      • /ab{2,4}c/ 匹配 a 和 c 中间2到4个b
    • [abc] 匹配 a b c 中任一字符
      • /ab[123]c/ 匹配 ab 和 c 中间出现1、2或者3
  • 纵向匹配的扩展:字符组——一个字符的集合
    • - 表示范围
      • /[a-d2-5D-I]/[abcd2345DHI] 的省略简写模式
      • 转义问题:\- 用来匹配 - 字符本身
    • ^ 表示取反
      • /[^abc]/ 可以匹配 x 但不匹配 abc
      • Q:^ 只能放在纵向匹配方括号中吗?
    • 常见缩写——大小写互为补集
      • \d = [0-9] ::Digit::
      • \D = [^0-9]
      • \w = [0-9a-zA-Z_] 数字、大小写字母和下划线 ::Word::
      • \W = [^0-9a-zA-Z]
      • \s = [\t\v\n\r\f] 表示空白符,包括空格、水平制表符
      • 换行符、回车符、换页符::Space::
      • \S
      • . = [^\n\r\u2028\u2029]通配符,几乎匹配任何字符,换行符、回车符、行分隔符和段分隔符除外
      • [\d\D] [\w\W] [\s\S] [^] 可以表示匹配任何字符
  • 横向匹配的扩展:量词
    • 简写
      • {m,} 最少 m 次
      • {m} 精确的 m 次
      • ? = {0,1} 出现一次或0次
      • + = {1,} 最少出现一次
      • * = {0,} 可以不出现或出现不限次数
    • 贪婪匹配和惰性匹配
      • 在量词后边加?可以改为惰性匹配
      • Q:差别不是特别清楚 贪婪匹配会匹配出尽可能长的字符串,惰性则是尽可能短的,符合匹配就返回
      • /\d{2,4}/g 匹配 123 1234 12345 123456 结果:123 1234 1234 1234 56
      • /\d{2,4}?/g 匹配 123 1234 12345 123456 结果: 12 12 34 12 34 12 34 56
      • /\d+/ 匹配 123 2 结果:123 2
      • /\d+?/ 匹配 123 2 结果:1 2 3 2
  • 多选分支
    • | 管道符可以在多个模式中任选其一匹配
      • /good|goodbye/ 匹配到good就会返回,而不会匹配到goodbye,所以管道符号会短路,有匹配到就不会继续匹配

2.位置匹配攻略

理解「位置」:可以认为位置是字符串中字符与字符之间的“空字符”
例如

1
2
3
"hello" === "" + "hello" + ""
"hello" === "" + "" + "hello"
"hello" === "h" + "" + "ello"

匹配位置就是匹配这些空字符串,这些空字符串可以做替换

  • ^ $ 匹配开头结尾

    • 把字符串的开头/结尾替换成# 'hello'.replace(/^|$/g, '#') // '#hello#'
    • 多行匹配时,两个字符是的开头结尾
      1
      2
      3
      4
      5
      6
      // g: 全局匹配,m: 多行匹配
      'hello\nworld'.replace(/^|$/gm, '#')
      /*
      * #hello#
      * #world#
      */
  • \b 匹配单词边界,\w\W^$ 的边界 ::Boundary::

    • 虽然被称为“单词”,但 \w 代表数字字母下划线,不仅仅是字母
    • 一个文件名为”[JS] Lesson_01.mp4”,其单词边界为
      1
      2
      '[JS] Lesson_01.mp4'.replace(/\b/g, '#')
      // [#JS#] #Lesson_01#.#mp4#
  • \B 取反

    1
    2
    '[JS] Lesson_01.mp4'.replace(/\B/g, '#')
    // #[J#S]# L#e#s#s#o#n#_#0#1.m#p#4
  • (?=p) 匹配 p 左边的位置,p表示一个子模式

    • positive lookahead,正向先行断言
    • 匹配 l 之前的位置
      1
      2
      'hello'.replace(/(?=l)/g, '#')
      // he#l#lo
  • (?!p) 匹配与 (?=p) 取反

    • negative lookahead,负向先行断言
  • ES6 还支持 (?<=p)(?<!p) 表示匹配右边的位置

案例:数字的千分位分隔符匹配法

要求把 ”1234567” 变成 “1,234,567”

'1234567'.replace(reg, ',')

思路是匹配连续三个数字\d{3},在它们前边加逗号。有两个问题:

  • 如何从后往前匹配
    • /(?=\d{3})/ 这样会替换成 ,1,2,3,4,567,原因是从前向后依次匹配到了 123 234 345 456, 所以1 2 3 4 5前边都加上了逗号
    • 增加结尾匹配 /(?=\d{3}$)/,这样会在结尾替换出1234,567
      • 如果把 $ 写在括号外边:/(?=\d{3})$/ 则表示匹配连续三个数字之前的位置,但是这个位置之后就是结尾。这应该是一种无效匹配
    • 再给\d{3}整体加上量词 /(?=(\d{3})+$)/,就能匹配多组数字了
      • 1,234,567
  • 如何避免开头有逗号
    • 前边的表达式替换123456789会出现,123,456,789,需要避免匹配到开头^
    • (?!^)可以匹配非开头(还是没明白为啥…),个人觉得应该有一个类似匹配字符的 ^ 来表示取反,而不是用位置来表示“开头”。开头和结尾这两个位置的定义还是很奇妙。
    • /(?!^)(?=(\d{3})+$)/ 可以替换出 123,456,789
    • ::位置匹配如何做到「非」?:: ::两个位置连用 代表对同一个位置的「且」?::看上去两个位置连用的确表示一起描述同一个位置

扩展:要求匹配 123456 12345678

/\B(?=(\d{3})+\b)/g

学习正则匹配有点像是学自然语言而不是编程语言——先学会实践怎么用,而不是学习如何实现(语法结构)

括号的作用

分组

要匹配连续的字符a可以写 /a+/,如果想匹配连续的ab,需要括号/(ab)+/,括号提供分组功能

分支结构

| 表示分支结构时,可选项的范围需要由括号包围。
/(JS|Java) is the best lang/ 匹配 JS is the best lang 或者 Java is the best lang
如果没有括号 /JS|Java is the best lang/ 匹配 JS 或者 Java is the best lang,整个表达式都是分支

引用分组——JS实现

括号可以进行数据提取,必须配合使用实现环境的 API。下边以 JS 为例
简单来说就是可以将表达式中的匹配项以 JS 变量的方式获取到。

RegExp.prototype.execString.prototype.matchString.prototype.replace 三个函数都会得到一个数组(前两个是返回值,后一个会传给 lambda 的入参)

1
2
/(\d{4})-(\d{2})-(\d{2})/.exec('2021-04-29')
// ['2021-04-29', '2021', '04', '29', index: 0, input: '2021-04-29', groups: undefined]

其中1~3个元素就是括号提取出的数据,第0个表示正则表达式匹配到的字符串。

另外提一句,返回值是一个添加了自定义属性的数组。Array 是 Object 的实例,自带 length 属性,也可以像这里的返回值一样增加 indexinputgroups 属性。

前边的例子中,调用 replace 之后也可以通过 RegExp.$1 RegExp.$2 RegExp.$3 拿到对应三个数据,不过这种方式已经废弃。这种实例能改变对象本身的变量,设计也很奇怪,不再深究。

反向引用

指表达式前边声明过的模式,后边有一份一模一样的引用

1
2
3
// 匹配 2021-04-29 2021/04/29 2021.04.29
// 要求前后两个符号一模一样,不能是 2021/04.29
/\d{4}(-|\/|\.)\d{2}\1\d{2}/

其中的 \1 就表示第一个分组(-|\/|\.)不论第一个分组匹配到什么,\1 的内容都一模一样。注意这里和复制粘贴一份 (-|\/|\.) 到后边有本质区别,反向引用使得两个模式有了关联。::为啥这里写([-/.])不行?::
随着分组数量增加,\1 \2 \3 后边的数字也增加

  • 括号嵌套时,左括号的次序代表\后边的数字
    1
    2
    /^((\d)(\d(\d)))\1\2\3\4$/.exec("4564564566")
    // [4564564566, 456(\1), 4(\2), 56(\3), 6(\4) ...]
  • \10 表示的是第10个分组,而不是\10
  • 引用不存在的分组时,\1 保持字面意思,代表一个字符

再另外提一句,在 JS 的字符串中,反斜杠\有转义的作用

  1. 转义字符:\0 \b \f \n \r \t \v \' \" \\
  2. \HHH 后边跟三个 000377八进制数代表一个字符,HHH 代表对应 Unicode 码点(code point),一共能输出256种字符。\1 === \001 遵循这个规则
  3. \xHH 后边跟 00FF十六进制数,同样代表对应 Unicode 码点,能输出256种字符
  4. \uXXXX 后边也跟 0000FFFF十六进制数XXXX 代表对应码点
  5. 如果反斜杠后边不是特殊字符,则反斜杠会被省略 '\a' === 'a'
1
2
3
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true

非捕获分组

可以用(?:p)来避免分组引用,括号内的匹配不会被提取,而只是作为分组或者分支结构用 /(?:a|b)(\d+)/ 不会引用a或b,只会引用到后边的数字