mengtuifrontend / Blog

芦叶满汀洲,寒沙带浅流。二十年重过南楼。柳下系船犹未稳,能几日,又中秋。 黄鹤断矶头,故人今在否?旧江山浑是新愁。欲买桂花同载酒,终不似,少年游。
18 stars 5 forks source link

打开正则的正确姿势 #23

Open shenxuxiang opened 5 years ago

shenxuxiang commented 5 years ago

正则

引言

作为一名合格的前端开发工程师,了解并掌握正则表达式是非常有必要的。多年的项目经验告诉我,学好正则表达式可以让我们少写很多的代码。这篇文章非常适合哪些初中级和对正则表达式掌握不是很清楚的同学,废话不多少,我门开始吧。。。

正则表达式的定义

js中的正则表达式用 RegExp 对象表示,可以使用 RegExp() 构造函数来创建 RegExp 对象,不过更多情况下我们使用一种特殊的直接量语法来创建,举个例子:

  // 使用直接量的形式
  var pattern = /\d$/;
  // 使用构造函数的形式
  var pattern = new RegExp('\d$');

但是如果需要我们动态的生成一个正则表达式呢,那么这个时候就需要使用构造函数形式了, 看下面的这个例子

  var fmt = 'MM-dd hh:mm:ss';
  var date = new Date();
  var fmtObj = {
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds(),
  }
  for(let key in fmtObj) {
    if (new RegExp(`(${key})`).test(fmt)) {
      fmt = fmt.replace(RegExp.$1, RegExp.$1.lengt === 1 ? fmtObj[key] : ('00' + fmtObj[key]).substr((fmtObj[key] + '').length))
    }
  }

直接量字符

正则表达式中所有的字母和数字都是按照字面量的含义进行匹配的,js的正则表达式也支持非字母的字符匹配,这些字符需要功过反斜杠 \ 进行转译。下表中列举了转译字符

\o NUL字符(\u0000)
\t 制表符(\u0009)
\n 换行符 (\u000A)
\v 垂直制表符(\u000B)
\f 换页符(\u000C)
\r 回车符(\u000D)
\xnn 由16进制数nn指定的拉丁字符,例如,\x0A等价于\n
\uxxxx 由16进制数xxxx指定的unicode字符,例如\u0009等价于\t
\cX 控制字符^X,\cJ等价于换行符\n
字母和数字字符 匹配自身

特殊含义的符号和字符

在正则表达式中,许多标点符号具有特殊的含义,他们是:^ $ . * + ? = ! : | \ / ( ) { } [ ]

某些符号只能在正则表达式的某些上下文中才具有某种特殊含义,在其他上下文中则会被当成直接量处理。如果想要在正则表达式中使用这些符号的直接量进行匹配,则必须使用前缀 \ ,这是一条通用的规则,其他标点符号(例如@和引号)没有特殊含义,在正则表达式中按照字面量含义进行匹配,如果不记得哪些标点符号需要反斜杆转义,可以在每一个标点符号前面都加上转义符 \ 。另外需要注意,许多字母和数字在有反斜杆做前缀时会有特殊含义,所以对于想按照直接量进行匹配的数字和字母,尽量不要用 \ 进行转义

字符类

将直接量字符单独放进方括号内就组成了字符类。一个字符类可以匹配它所包含的任意字符,因此表达式 /[abc]/ 就和字母a,b,c中的任意一个都匹配

  var reg = /[a-z0-9]/;

上面的正则表达式可以匹配任意一个字母或数字,其中 a-z 表示a到z的所有字母,0-9 表示0到9之间的数字

反字符类[^...]

就是匹配字符类中不含有的字符,用 ^ 符号开头

    var reg = /[^a-z0-9]/;

上面的正则表达式的意思就是匹配任意一个不含有字母和数字的字符

正则表达式的字符类

[...] 字符类
[^...] 反字符类
. 除换行符和其他unicode行终止符之外的任意字符
\w 数字,字母和下划线, 等价于[a-z0-9_]
\W 非数字,字母和下划线, 等价于[^a-z0-9_]
\s 任何unicode 空白符
\S 非任何unicode 空白符
\d 数字
\D 非数字
\b 单词边界
\B 非单词边界

正则表达式的重复语法

{n, m} 最少匹配n次,最多匹配m次
{n, } 至少匹配n次或更多
{n} 匹配n次
? 匹配0次或一次
+ 最少匹配一次
* 匹配0次或多次

非贪婪模式

上面我们列举出了匹配重复字符是尽可能多的匹配,而且允许后续的正则表达式继续匹配。因此我们称之为'贪婪'匹配模式。我们同样可以使用的正则表达式进行非贪婪匹配。只需要在待匹配的字符后跟随一个问号即可: '??', '+?', '*?' 或者'{1, 5}?'

  var a = /[a-z0-9]+?/
  a.exec('sdfff900') // ["s", index: 0, input: "sdfff900", groups: undefined]

  // 使用非贪婪模式匹配得到的结果可能和期望并不一样,可以来看看看下面的这个例子:
  var b = /a+?b/;
  b.exec('aaab') // ["aaab", index: 0, input: "aaab", groups: undefined]

  // 下面这样的匹配结果你想到了吗??
  var c = /shen(ab)+?/;
  b.exec('shenababab'); // ["shenab", "ab", index: 0, input: "shenababab", groups: undefined]

这是因为正则表达式的模式匹配总是会寻找字符串中第一个可能匹配的位置,由于这个匹配是从字符串的第一个字符开始的,因此在这里不考虑他的子串中更短的匹配

选择,分组和引用

正则表达式语法还包括指定选择项,子表达分组和引用前一子表达式的特殊字符,我们分别来看一下

选择项:| 用于分隔供选择的字符。

/ab|cd|ef/,可以匹配字符串ab,也可以匹配cd或者是ef。

注意选择项的尝试匹配的次序是从左到右,直到发现了匹配的项。如果左边的选择项匹配,就忽略右边的选择项,即使产生更好的匹配。 所以,当正则表达式 /a|ab/ 匹配字符串 ab 时,他只能匹配第一个字符串 a

子表达式:

正则表达式中的圆括号有多种作用。

把单独的项组合成子表达式,以便可以像处理单独的单元那样用 | * + ? 等来对单元内的项进行处理。
  var a = /java(script)?/; // 可以匹配字符串 'java',其后可以有'script'也可以没有。
  var b = /(ab|cd)+|ef/; // 可以匹配字符串ef, 也可以匹配ab或者cd的一次或多次重复
在完整的模式中定义子模式,当一个正则表达式和目标字符串成功匹配时,可以从目标字符串中拿到和圆括号中的字符模式相匹配的部分。
  var a = /[a-z]+(\d+)/; // 那么这个时候我们可以从检索到的匹配中拿到和圆括号中子模式相匹配的数字

() 表示捕获分组,() 会把每个分组里的匹配的值保存起来,使用 $n (n是一个数字,表示第n个捕获组的内容)

  var reg = /\b(shen(sxx))/;
  reg.test('shensxx');
  console.log(RegExp.$1, RegExp.$2) // shensxx sxx

  reg.exec('shensxx');
  // ["shensxx", "shensxx", "sxx", index: 0, input: "shensxx", groups: undefined]

(?:) 表示非捕获分组,和捕获分组唯一的区别在于,非捕获分组匹配的值不会保存起来

  var reg = /\b(shen(?:sxx))/;
  reg.test('shensxx');
  console.log(RegExp.$1, RegExp.$2) // shensxx

  reg.exec('shensxx');
  // ["shensxx", "shensxx", index: 0, input: "shensxx", groups: undefined]

引用:在同一正则表达式的后部引用前面的子表达式

这是通过在字符 \ 后面加一位数字或多位数字来实现的,这个数字指定了圆括号的子表达式在正则表达式中的位置。\1, \3 分别表示正则表达式中的第一个圆括号的子表达式和第三个子表达式(注意这里指的是自表达式中匹配的文本的引用,并不是指子表达式)

注意: 因为子表达式可以嵌套另一个子表达式,所以他们的位置是参与计数的左括号的位置

  var a = /([Jj]ava([Ss]cript)?)\sis\s(fun\w*)/; // \2 则是代表的([Ss]sript)

    var b = /(['"])[^'"]*\1/; // 只能匹配单引号或者双引号是成对出现的,不允许出现一个单引号一个双引号

指定匹配位置

有一些正则表达式匹配的是字符串之间的位置,而不是实际的字符。例如 \b 匹配一个单词边界,即位于 \w\W 之间的边界,或者是一个字符串开始或结束的位置。像 \b 这样的元素不匹配某个可见的字符,他们指定匹配发生的合法位置。还有一些锚元素 ^ $, 分表表示匹配字符串开始和结束的位置

\b 匹配一个单词边界,即位于 \w\W 之间的边界,或者是一个字符串开始或结束的位置。

  var reg = /xx\b/; 
  reg.test('sxx shen') // true
  reg.test('xxs shen') // false

  var reg = /\bsx/;
  reg.test('sxx shen') // true
  reg.test('xsx shen') // false

  var reg = /\b\d+/;
  reg.test('.123') // true 
  reg.test('sxx124') // false

\B 匹配非单词边界。er\B 能匹配 verb 中的 er,但不能匹配 never 中的 er

x(?=y) :正向先行断言,匹配 x 仅仅当 x 后面不跟着 y

x(?!y) :负向先行断言, 匹配 x 仅仅当 x 后面不跟着 y

我们看下面的正则表达式,意思就是非单词边界后面跟这三个数字字符串,而且三个数字字符串后面不再跟数字

  var reg = /\B(?=(\d{3})+(?!\d))/g;
  '123456789.123456'.replace(reg, ',') // "123,456,789.123,456"

(?<=y)x :正向后行断言,匹配 x 仅仅当 x 前面跟着 y

  var reg = /(?<=95|98|NT|2000)Windows/;

  '3.1Windows'.replace(reg, 'aaaa'); // '3.1Windows'没有匹配到

  '2000Windows'.replace(reg, 'aaaa'); // '2000aaaa'

(?<!y)x :负向后行断言,匹配 x 仅仅当 x 前面跟的不是 y

  var reg = /(?<!95|98|NT|2000)Windows/;

  '3.1Windows'.replace(reg, 'aaaa'); // '3.1aaaa'

  '2000Windows'.replace(reg, 'aaaa'); // '2000Windows' 没有匹配到

看个demo,不在小数点后加千分符

  var reg = /(?<!\.\d*)\B(?=(\d{3})+(?!\d))/g

  '123456789.98764525437'.replace(reg, ',')

  // "123,456,789.98764525437"

修饰符

i: 执行时不区分大小写 g: 执行一个全局匹配,简而言之,就是找到所有的匹配,而不是在找到第一个之后就停止 m: 多行匹配模式,^ 匹配一行的开头和字符串的开头,$ 匹配行的结束和字符串的结束

使用字面量形式时: /\bjavascript\b/ig 构造函数形式: new RegExp('\bjavascript\b', 'ig')

用于模式匹配的String方法,string支持4种使用正则表达式的方式

search() 不支持全局匹配,会忽略修饰符g

@params: 一个正则表达式,如果参数不是一个正则表达式,那么会通过 `RegExp` 构造函数转成正则表达式
@return: 第一个与之匹配的字符串的起始位置,若是没有发生匹配就返回数字 `-1`
  var a = 'javascript'
  'fsfsdjavsssjavasdfsfsjfjhhshh3r98u'.search(a) // 11

  var a = /[Jj]ava([Ss]crit)*/g;
  '012345javascript 67890'.search(a) // 6

replace()

@params: RegExp | string (正则表达式,可以设置修饰符)
@params: function | string
@return: 返回一个新的字符串,不改变源对象

如果第一个参数是一个string,那么 replace() 将直接搜索这个字符串然后进行替换(注意这里指替换第一个搜索到的结果),并不会先转成 RegExp 进行匹配。 如果第一个参数是一个 RegExp,那么 replace() 将会进行正则匹配,将第一次匹配到结果进行替换,这里正则表达式可以设置修饰符。g 会进行全局多次匹配,将所有匹配到的结果进行替换

如果第二个参数是一个 string,那么 replace() 将匹配的结果直接用这个字符串替换 如果第二个参数是一个 function,我们看看可以有哪些参数: @params: 完整模式匹配到的结果 @params: 完整模式下的子模式匹配到的结果,看正则表达式中有多少个圆括号,那么这里就可以有多少个这样的参数,每个参数表示对应的子模式匹配的结果 @return: 使用 return 的返回值替换匹配到的值

    var a = /([Jj]ava([Ss]cript))\sis\s(fun\w*)/g;
    var b = 'ffhfhjavascript is functionsdfffsff';

    b.replace(a, function(match) {
        // 打印的结果就是 javascript is functionsdfffsff 123
        console.log(match, 123);
        return '@@@@'
    });
    // ffhfh@@@@

    b.replace(a, function(match, v1, v2, v3) {
        // 打印的结果就是 javascript is functionsdfffsff 123
        console.log(match, 123);
        // javascript
        console.log(v1);
        // script
        console.log(v2);
        // functionsdfffsff
        console.log(v3);

        return '@@@@'
    });
    // ffhfh@@@@

match()

@params: 正则表达式 | string(通过 `RegExp` 的构造函数转化成正则表达式)
@return: 数组,

如果这个正则表达式没有设置修饰符 gmatch() 就不会进行全局检索,只检索第一个匹配。在这种情况下:数组的第一个元素就是就是完整模式匹配的结果,其余的元素则是正则表达式中圆括号的子表达式匹配的结果(如果没有圆括号就不会有这些元素)。index 表示匹配的位置,input 表示目标字符串

  var a = /([Jj]ava([Ss]cript))\sis\s(fun\w*)/;
  var b = 'ffhfhjavascript is functionsdfffsff';

  b.match(a)
  // ["javascript is functionsdfffsff", "javascript", "script", "functionsdfffsff", index: 5, input: "ffhfhjavascript is functionsdfffsff", groups: undefined]

如果添加了修饰符g,那么返回的数组中就是多次全局模式匹配的结果

  var a = /123(sxx)456(hello)/g;
  var b ='123sxx456helloffffjfljfj123sxx456hellojjweww123456hello123sxx456hello';

  b.match(a)
  // ["123sxx456hello", "123sxx456hello", "123sxx456hello"]

RegExp的两种方法:exec() 和 test()

exec()

@params: string
@return: 数组 | null

如果匹配到结果就返回一个数组,结果和字符串的 match() 方法的非全局匹配返回的结果一样。但是有一点和 match() 不一样,就是不管式全局还是非全局都是一样的结构。如果没有匹配到结果就返回 null

  var a = /123(sxx)456(hello)/g;
  var b ='123sxx456helloffffjfljfj123sxx456hellojjweww123456hello123sxx456hello';
  a.exec(b);
  // [“123sxx456hello", "sxx", "hello", index: 0, input: "123sxx456helloffffjfljfj123sxx456hellojjweww123456hello123sxx456hello", groups: undefined]

test()

@params: string
@return: boolean

这个方法很简,匹配成功就返回true,否则就返回false

参考

javascript权威指南