JS: RegExp(正则表达式)

RegExp语法(包含ES2018标准)

注意:本次所有代码都仅在Chrome 70中进行测试

  1. 正则表达式是什么?

    正则表达式是用于匹配字符串中字符组合的模式。(mdn)

    简单来说,正则表达式是用来提取、捕获文本(匹配字符)的。

  2. 创建:

    • 字面量:let regex = / pattern / flags

      let regex1 = /foo/i;
    • 构造函数:let regex = new RegExp(pattern, falgs);

      let regex2 = new RegExp('bar', ig); // ES5
      let regex3 = new RegExp(/bat/im); // ES5
      let regex4 = new RegExp(/cat/ig, 'g'); // ES6
      /* regex4 创建方法在ES5中会抛出TypeError,因为第一个参数已经是一个正则表达式,而ES5不允许此时再使用第二个参数添加修饰符。ES6则允许这种写法,但第二个参数会作为修饰符覆盖第一个参数中的修饰符。*/
      console.log(regex4); // /cat/g
  3. 实例属性:

    每个正则表达式实例都拥有下面的属性,以便获取实例模式的信息。

    • global:布尔值,表示是否设置了 g(全局匹配)标志。

    • ignoreCase:布尔值,表示是否设置了 i(忽略大小写)标志。

    • multiline:布尔值,表示是否设置了 m(多行)标志。

    • unicode:布尔值,表示是否设置了 u(识别 unicode 字符中大于\uFFFF的 unicode 字符)标志。

    • sticky:布尔值,表示是否设置了 y(粘连)标志。

    • lastIndex:上次成功匹配后的索引位置,会成为下次匹配的开始索引位置,只在全局匹配或粘滞匹配模式下可用。

    • source:正则表达式中pattern (模式)的字符串表示,与调用toString()或者valueOf()方法得到的结果并不一样。

    • flags:返回正则表达式中flags(修饰符)的字符串表示。

    • dotAll:返回一个布尔值,表示是否设置了 s(dotAll)标志。

      let str2 = 'batfoocat';
      let pattern2 = /at/g;
      
      pattern2.global;  // true
      pattern2.sticky;  // false
      pattern2.source; // at
      pattern2.flags; // g
      pattern2.toString(); // /at/g
      pattern2.valueOf(); // /at/g
      pattern2.lastIndex; // 0
      
      let matches = pattern2.exec(str2); // 第一次
      matches[0]; // at
      matches.index; // 1
      pattern2.lastIndex; // 3
      
      matches = pattern2.exec(str2); // 第二次
      matches[0]; // at
      matches.index; // 7
      pattern2.lastIndex; // 9
      
      /*第三次会出现报错,是因为已经没有匹配项了,exec()方法返回了null,再执行第四次就会返回第一次匹配的结果,即重新开始匹配*/
      matches = pattern2.exec(str2); // 第三次
      matches[0]; // error
      matches.index); // error
      pattern2.lastIndex; // 0

      补充:已经废弃的属性(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Deprecated_and_obsolete_features)

      这些废弃的特性仍然可以使用,但你要保持谨慎,因为它们很可能会在未来的某个时候被删除.(mdn)

  4. 方法:

    • exec:在指定字符串中进行匹配字符,每次只会返回一个匹配项的信息。

      匹配成功,则返回一个数组,并更新正则表达式实例的属性,否则返回 null。

      返回的数组是 Arrary 实例,但包含了两个属性:index(匹配项在字符串中的位置)和 input(正则表达式进行匹配的字符串),数组第一项(下标0)存放匹配到的文本。

      注意:如果使用了全局匹配(g),再次使用exec()方法会返回第二个匹配项的信息,否则无论使用多少次exec()方法都只会返回第一个匹配项信息。

      补充:ES2018在返回数组中新增了一个属性groups(命名捕获组的信息)

      let str1 = 'batfoocat';
      let pattern1 = /at/g;
      pattern1.exec(str1); // 第一次
      // ["at", index: 1, input: "batfoocat", groups: undefined]
      pattern1.exec(str1); // 第二次
      // ["at", index: 7, input: "batfoocat", groups: undefined]
      pattern1.exec(str1); // 第三次
      // null
      // 第四次会重新开始匹配,即返回第一次匹配的结果
    • test():测试当前正则表达式是否能匹配目标字符串,返回布尔值。

      let str3 = 'batfoocat';
      let str4 = 'abcde';
      let pattern3 = /at/g;
      pattern3.test(str3); // true
      pattern3.test(str4); // false
    • String.prototype.search():检索与正则表达式相匹配的子字符串,匹配成功返回第一个匹配项在字符串中的下标,否则返回-1。

      let str5 = 'abcdea';
      str5.search(/a/g); // 0
      str5.search(/f/g); // -1
    • String.prototype.match():检索与正则表达式相匹配的子字符串,匹配成功返回一个存放所有匹配项的数组,否则返回null,如果正则表达式中没有标志 g(全局标志),那么match()方法就只能执行一次匹配。

      注意:在全局检索模式下,match() 即不提供与子表达式匹配的文本的信息,也不声明每个匹配子串的位置。如果需要这些全局检索的信息,可以使用RegExp.exec()

      let str6 = 'abcdea';
      str6.match(/a/g);
      // ["a", "a"]
      str6.match(/a/);
      // ["a", index: 0, input: "abcdea", groups: undefined]
      str6.match(/f/g);
      // null
    • String.prototype.replace(regexp, replacement):替换一个与正则表达式匹配的子串。

      let str7 = 'batfoocat';
      let a = str7.replace(/at/g, 'oo');
      // "boofoocoo"
      
      let b = str7.replace(/at/, 'oo');
      // "boofoocat"
      
      let c = str7.replace(/at/g, (value)=> {
          return  '!' + value;
      });
      // "b!atfooc!at"
    • String.prototype.split(separator [, howmany]):把一个字符串分割成字符串数组,第二个参数为可选,该参数可指定返回的数组的长度,不填则返回所有。

      let str8 = 'batfoocat';
      let a = str8.split(/at/g); // ["b", "fooc", ""]
      let b = str8.split(/at/); // ["b", "fooc", ""]
      let c = str8.split(/at/, 2); // ["b", "fooc"]
  5. 修饰符(标志 – flags):

    • g:全局匹配,找到所有匹配,而不是在发现第一个匹配项后立即停止。

      let str9 = 'batfoocat';
      str9.match(/at/);
      // ["at", index: 1, input: "batfoocat", groups: undefined]
      str9.match(/at/g);
      // ["at", "at"]
    • i:忽略大小写。

      let str10 = 'AabbccDD';
      str10.match(/a/gi); // ["A", "a"]
      str10.match(/a/g); // ["a"]
      str10.match(/A/g); // ["A"]
    • m:执行多行匹配,和^$搭配起来使用。

      多行; 将开始和结束字符(^和$)视为在多行上工作(也就是,分别匹配每一行的开始和结束(由 \n 或 \r 分割),而不只是只匹配整个输入字符串的最开始和最末尾处。(mdn)

      `
      abc
      def
      `.match(/def/);
      // ["def", index: 5, input: "↵abc↵def↵", groups: undefined]
      
      `
      abc
      def
      `.match(/def/m);
      // ["def", index: 5, input: "↵abc↵def↵", groups: undefined]
      
      `
      abc
      def
      `.match(/^def$/);
      // null
      
      `
      abc
      def
      `.match(/^defc$/m);
      // ["def", index: 5, input: "↵abc↵def↵", groups: undefined]
    • u:Unicode 模式,可以正确处理码点大于\uFFFF的 Unicode 字符。

      /\u{20BB7}/.test('𠮷'); // false
      /\u{20BB7}/u.test('𠮷'); // true
      '𠮷'.match(/./);
      // ["�", index: 0, input: "𠮷", groups: undefined]
      '𠮷'.match(/./u);
      // ["𠮷", index: 0, input: "𠮷", groups: undefined]

      补充:使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。

      /𠮷{2}/.test('𠮷𠮷') // false
      /𠮷{2}/u.test('𠮷𠮷') // true
    • y:与g一样是全局匹配,但存在粘性匹配特点,即每次都从 lastIndex 位置开始新的匹配。

      粘性匹配,仅匹配目标字符串中此正则表达式的 lastIndex 属性指示的索引(并且不尝试从任何后续的索引匹配)。(mdn)

      let str11 = 'batcatdat';
      str11.match(/at/g);
      // ["at", "at", "at"]
      
      str11.match(/at/y);
      // null
      /*初始 lastIndex 为0,所以 y 的粘连让正则表达式从 str11 索引值为0的 b 开始匹配,不符合正则表达式中要匹配的 at,所以匹配失败,返回null*/
      
      str11.match(/at/gy);
      // null
      str11.match(/\wat/y);
      // ["bat", index: 0, input: "batcatdat", groups: undefined]
      str11.match(/\wat/gy);
      // ["bat", "cat", "dat"]
    • s:dotAll 模式,和.搭配使用,ES2018新增特性。

      正则表达式中,.是代表任意的单个字符,但有两种字符是无法匹配的:一个是四个字节的 UTF-16 字符(ES6通过引入u修饰符解决),另一个是行终止符(即表示一行的终结,例如回车符 \r、换行符\n等)。为了解决这个问题,ES2018引入了s修饰符。

      'bat\ncat'.match(/bat\ncat/);
      // ["bat↵cat"]
      'bat\ncat'.match(/bat.cat/);
      // null
      'bat\ncat'.match(/bat.cat/s); 
      // ["bat↵cat", index: 0, input: "bat↵cat", groups: undefined]

      行结束符:\n \r \u2028\u2029。(mdn)

  6. 转义

    如果正则表达式的匹配模式里有元字符:( [ {  ^ $ | ? * + . } ] ),需要使用反斜杠\进行转义才能进行正常的匹配。

    /.*?/.exec('question?');
    // ["", index: 0, input: "question?", groups: undefined]
    
    /.*\?/.exec('question?');
    // ["question?", index: 0, input: "question?", groups: undefined]
  7. 元字符

    • 边界

      边界 含义
      ^ 匹配输入开始,即如果^作为正则表达式的第一个符号,那在^后面的字符必须是被匹配文本(即被正则表达式匹配的原始字符串)的第一个字符
      $ 匹配输入结束,即如果$作为正则表达式的最后一个符号,那在$前面的字符必须是被匹配文本的最后一个字符
      \b 见下表
      \b 见下表

      注意:边界指的是匹配的不是字符而是一个位置。

      'abcde'.match(/^abc/);
      // ["abc", index: 0, input: "abcde", groups: undefined]
      'fabcde'.match(/^abc/);
      // null
      
      'abcde'.match(/e$/);
      // ["e", index: 4, input: "abcde", groups: undefined]
      'abcdef'.match(/e$/);
      // null
    • 带反斜杠\的常用元字符

      元字符 含义
      \b 匹配一个单词边界
      \B 匹配一个非单词边界
      \d 匹配一个阿拉伯数字字符,等价于[0-9]
      \D 匹配一个非阿拉伯数字字符,等价于[^0-9]
      \s 匹配一个空白符
      \S 匹配一个非空白符
      \w 匹配一个字母或者数字或者下划线,等价于 [A-Za-z0-9_]
      \W 匹配一个字母、数字、下划线以外的字符,等价于 [^A-Za-z0-9_]

      可以看得出来,大写与小写各代表的意思是相反的。

      注意1:除\b\B外,其余三个元字符将大小写放在一起,可以匹配任意字符。

      'a b'.match(/[\s\S]/g);
      // ["a", " ", "b"]
      'a b'.match(/[\W\w]/g);
      // ["a", " ", "b"]
      'a b'.match(/[\D\d]/g);
      // ["a", " ", "b"]
      'a b'.match(/[\B\b]/g);
      // null

      注意2:\b对中文是无效的。

      'The future is in our own hands'.match(/\bfuture\b/);
      // ["future", index: 4, input: "The future is in our own hands", groups: undefined]
      '你好 我好 大家好'.match(/\b好\b/g);
      // null

      注意3:\s用于匹配空白符,而空白符包含下列所有字符,而这些空白符自身也是_元字符_,可以用于正则表达式中。

      • ' '空格符 (space character – 就是一个空格)
      • \t水平制表符 (tab character)
      • \r回车符 (carriage return character)
      • \n换行符 (new line character)
      • \v垂直制表符 (vertical tab character)
      • \f换页符 (form feed character)
      'a b'.match(/\w\s\w/);
      // ["a b", index: 0, input: "a b", groups: undefined]
      
      'a b'.match(/\w \w/);
      // ["a b", index: 0, input: "a b", groups: undefined]
      
      `
      a
      b
      `.match(/\w\n\w/);
      // ["a↵b", index: 1, input: "↵a↵b↵", groups: undefined]
    • 点(.

      .可以匹配任意单个字符,但有两种字符是无法匹配的:一个是四个字节的 UTF-16 字符(ES6通过引入u修饰符解决),另一个是行结束符(ES2018引入了s修饰符解决)。而且在字符集中,.失去其特殊含义,并匹配一个真正的.字符。

      '$@hhhh'.match(/.*/);
      // ["$@hhhh", index: 0, input: "$@hhhh", groups: undefined]
    • 量词

      量词 含义
      ? 匹配零次或者一次
      + 匹配一次或者多次
      ***** 匹配零次或者多次
      {n} 匹配n次
      {n,} 至少匹配n次,即匹配大于或等于n次
      {n,m} 匹配n次到m次之间的次数,包含n次和m次,即匹配x次(n<= x && x<=m)
      x|y 匹配x或者y

      注意1:正则表达式使用量词匹配字符的话,会匹配尽可能多的字符,即正则默认具有贪婪模式,如果要匹配尽可能少的字符,可以在量词后面加上?取消贪婪模式。

      '$@hhhh'.match(/.+/); // 贪婪模式
      // ["$@hhhh", index: 0, input: "$@hhhh", groups: undefined]
      
      '$@hhhh'.match(/.+?/); // 懒惰模式
      // ["$", index: 0, input: "$@hhhh", groups: undefined]

      注意2:{n,m}等几个使用大括号的,大括号里面不能有空格。

      '$@hhhh'.match(/.{1,3}/); // 没有空格
      // ["$@h", index: 0, input: "$@hhhh", groups: undefined]
      
      // '$@hhhh'.match(/.{1, 3}/); // 有空格
      null

      注意3:量词后除可以加?用来取消贪婪模式外,不能加任何量词。

      '$@hhhh'.match(/.{1,3}+/);
      // Uncaught SyntaxError
    • 字符组(集合、分组)

      字符组 含义
      [xyz] 一个字符组
      [^xyz] 一个反义的字符组
      (xyz) 一组字符集,即圆括号内的字符是一个整体
      /* [abc] 里面的 a、b、c 只是作为正则表达式匹配字符时的可选项,[abc]只会匹配一个字符,除非使用修饰符 g。*/
      'abc'.match(/[abc]/);
      // ["a", index: 0, input: "abc", groups: undefined]
      'abc'.match(/[abc]/g);
      // ["a", "b", "c"]
      'abc'.match(/[ae]/);
      // ["a", index: 0, input: "abc", groups: undefined]
      
      'abc'.match(/[^ae]/);
      // ["b", index: 1, input: "abc", groups: undefined]
      
      /* (abc) 里面的字符则是一个整体,(abc) 会匹配 abc 并且捕获匹配项。*/
      'abc'.match(/(abc)/);
      // ["abc", "abc", index: 0, input: "abc", groups: undefined]
      'abc'.match(/(abf)/);
      null
      'ab c ab ab'.match(/(ab)+/g);
      // ["ab", "ab", "ab"]

      字符组之间可以使用连字符-

      'abc123'.match(/[0-9]/);
      // ["1", index: 3, input: "abc123", groups: undefined]
      
      'abc123'.match(/[a-z]/);
      // ["a", index: 0, input: "abc123", groups: undefined]
      
      'abc123'.match(/[0-z]*/);
      // ["abc123", index: 0, input: "abc123", groups: undefined]
      /*数字与英文字母之间也可以使用连字符。*/
  8. 捕获组与非捕获组

    • 捕获

      上面的(xyz)提到了()会捕获匹配项,是因为使用了(),JavaScript的正则就会默认为它是捕获组,从而将()内的表达式匹配的内容捕获,并将捕获到的内容保存到内存中以数字命名的组里(ES2018新增了捕获命名),而这些保存的内容可以被引用,这就是反向引用

      /*在正则表达式内部引用捕获项,使用 \数字。*/
      '<a>example.com</a>'.match(/<(a)>.*<\/\1>/);
      // ["<a>example.com</a>", "a", index: 0, input: "<a>example.com</a>", groups: undefined]

      当有多个捕获组时,数字命名是从左到右从外往内增大的:

      'abc_d_e_d_abc'.match(/((a)(b(c))).*(d)/);
      /* ["abc_d_e_d", "abc", "a", "bc", "c", "d", index: 0, input: "abc_d_e_d_abc", groups: undefined]
      
      \1 = abc
      \2 = a
      \3 = bc
      \4 = c
      \5 = d
      
      在正则表达式外部也是可以引用捕获项的。
      */
      
      RegExp.$1;
      // "abc"
      RegExp.$2;
      // "a"
      RegExp.$3;
      // "bc"
      RegExp.$4;
      // "c"
      RegExp.$5;
      // "d"

      注意:在正则外部的引用是使用正则RegExp的构造函数属性来获取的,但这些构造函数属性已经被废弃。

      这些废弃的特性仍然可以使用,但你要保持谨慎,因为它们很可能会在未来的某个时候被删除. (mdn)

      补充:已经废弃的属性(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Deprecated_and_obsolete_features)

    • 非捕获

      在很多时候其实并不会引用捕获项,所以可以在()中加?:来取消捕获匹配项,以免造成内存的浪费。

      'batfoocat'.match(/(bat).*(?:cat)/);
      // ["batfoocat", "bat", index: 0, input: "batfoocat", groups: undefined]
      /*返回的数组里并没有 cat 的捕获项*/
    • 捕获命名

      ES2018引入了捕获命名,在()内加上?<name>就可以命名捕获组名,可以通过返回数组的groups属性获取。

      'batfoocat'.match(/(?<name_at>bat)/);
      // ["bat", "bat", index: 0, input: "batfoocat", groups: {name_at: "bat"}]
      
      /*不可以将命名放在匹配字符后面*/
      'batfoocat'.match(/(bat?<name_at>)/);
      // null
  9. 零宽断言

    零宽:仅仅匹配位置,并不作为结果返回。

    断言:判断,可以理解为布尔值,判断真假。

    ES2018引入了零宽后行断言。

    零宽断言 含义
    x(?=y) 零宽肯定先行断言,即只有当 x 后面跟着 y 才匹配 x
    x(?!y) 零宽否定先行断言,即只有 x 后面没有跟着 y 才匹配 x
    (?<=y)x 零宽肯定后行断言,即只有 x 前面有 y 才匹配 x
    (?<!y)x 零宽否定后行断言,即只有 x 前面没有 y 才匹配 x
    // 零宽肯定先行断言
    '1% 20'.match(/\d+(?=%)/);
    // ["1", index: 0, input: "1% 20", groups: undefined]
    
    // 零宽否定先行断言
    '1% 20'.match(/\d+(?!%)/);
    // ["20", index: 3, input: "1% 20", groups: undefined]
    
    // 零宽肯定后行断言
    'price: $1 ¥6'.match(/(?<=\$)\d+/);
    // ["1", index: 8, input: "price: $1 ¥6", groups: undefined]
    
    // 零宽否定后行断言
    'price: $1 ¥6'.match(/(?<!\$)\d+/);
    // ["6", index: 11, input: "price: $1 ¥6", groups: undefined]

    注意:零宽断言语法中括号里面的内容并不会被作为结果返回。

    小声bb:这断言的名字真是一言难尽,可能这就是官方术语吧。

  10. 运算符优先级

运算符(优先级从上往下、从左到右) 含义
\ 转义
(),(?:),(?=),[] 圆括号和方括号
*,+,?,{n},{n,},{n,m} 量词限定符
^,$,\任何元字符或任何字符
| 逻辑或
备注

语法虽然看着不难,但正则真正用起来感觉还是挺难的,不过真的很强大。