美文网首页JavaScript从入门到放弃
JavaScript 与正则表达式 -- 括号

JavaScript 与正则表达式 -- 括号

作者: 菜鸟很浮夸 | 来源:发表于2018-12-31 18:30 被阅读0次

    在正则表达式中,括号涉及的问题比较多,所以这里单独拿出来讲。

    分组

    如果量词所限定的元素不是一个字符或者字符组,而是一系列字符或者子表达式,就需要使用括号将他们括起来,表示为“一组”,构成单个元素

    var regex = /(ab)+/g;
    var string = "ababa abbb ababab";
    console.log( string.match(regex) );  
     //  ["abab", "ab", "ababab"]
    

    上面的例子中,量词 + 的前面的元素是 (ab) , 所以 + 所限定的是括号内 ab 这个整体。

    划定多选结构的范围

    多选结构, 也叫 分支结构。一般的用法: (p1|p2|p3),其中,| 表示 “或”,p1p2p3 是三个子表达式,这些子表达式也叫多选分支, 括号用来划定分支结构的范围。
    注意:多选结构中括号不是必须的。如果没有括号,管道符 | 会把整个表达式当做一个多选结构。比如,要匹配 grey或gray:

    var regexRight = /gr(e|a)y/;  // 匹配 grey 或 gray
    var regexWrong = /gre|ay/;  // 匹配 gre 或 ay
    
    // 正确的
    console.log(regexRight.test('grey'));  // true
    console.log(regexRight.test('gray'));  // true
    console.log(regexRight.test('gre'));  // false
    
    // 错误的
    console.log(regexWrong.test('grey'));  // true
    console.log(regexWrong.test('gre'));   // true
    

    所以,虽然多选结构中括号不是必须的,但是,通常会搭配括号来使用。

    多选结构与字符组

    上面多选结构中 gr(e|a)y的例子并太好,因为可以使用更好的方式代替,那便是 gr[ae]y,那么二者什么区别呢?
    二者差别还是很大的:

    • 多选结构中每个分支都必须明确列出。而字符组可以使用 - 表示范围
    • 大多数情况下, [abc] 要比 (a|b|c) 更高效
    • 字符组的每个 “分支” 都必须是单个的字符,而多选结构的“分支”可以是子表达式
    • 多选结构的分支顺序会影响到最后的配置结果
    • 没有 排除型多选结构

    引用分组

    使用括号之后,正则表示会保存每个分组真正匹配的文本,等匹配成功后,可以引用这些文本。
    因为这种情况下“捕获”了文本,所以这种分组叫 捕获分组,这种括号叫 捕获型括号

    通过编号引用

    编号规则:
    如,使用(\d{4})-(\d{2})-(\d{2})匹配日期 2018-12-30:

    字符串 2018 12 30
    表达式 (\d{4}) (\d{2}) (\d{2})
    分组编号 1 2 3

    注意:
    如果把表达式写成:(\d){4}-(\d){2}-(\d){2},则含义完全不同,(\d){4} 表示 \d 作为单独的元素出现4次,且编号都为1。

    嵌套规则:根据开括号的出现顺序来计数。(图参考《正则指引》P45,我画的有点丑)

    括号嵌套编号规则:开括号的出现顺序

    在 JavaScript 中使用

    提取数据

    String.prototype.match() 方法返回一个数组,数组的第一项是进行匹配的完整字符串,之后的项是捕获分组的匹配结果。

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    var text = '2018-12-30';
    console.log(text.match(regex));
    // ["2018-12-30", "2018", "12", "30", index: 0, input: "2018-12-30"]
    

    关于 match 方法,有一个地方需要注意,返回结果与正则表达式是否包含 g 标志有关。在没有 g 标志的时候,返回值和 regex.exec() 方法相同:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    var text = '2018-12-30';
    console.log(regex.exec(text));
    // ["2018-12-30", "2018", "12", "30", index: 0, input: "2018-12-30"]
    

    同时,也可以使用构造函数的全局属性 $1$9 来获取引用:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    var text = '2018-12-30';
    regex.exec(text);
    
    console.log(RegExp.$1);  // 2018
    console.log(RegExp.$2);  // 12
    console.log(RegExp.$3);  // 30
    

    替换

    比如,想把 yyyy-mm-dd 格式,替换成 mm/dd/yyyy 怎么做?
    可以使用下面的三种方法:

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    var text = '2018-12-30';
    
    // 1
    var result1 = text.replace(regex, '$2/$3/$1');
    
    // 2
    var result2 = text.replace(regex, () => `${RegExp.$2}/${RegExp.$3}/${RegExp.$1}`);
    
    // 3
    var result3 = text.replace(regex, (str, y, m, d) => `${m}/${d}/${y}`);
    
    console.log(result1);    // 12/30/2018
    console.log(result2);    // 12/30/2018
    console.log(result3);    // 12/30/2018
    

    String.prototype.replace() 规则相对复杂,有很多玩法,了解更多

    反向引用

    在正则表达式内部引用之前(左侧)捕获分组匹配的文本,形式如:\num ,其中 num 表示编号,编号规则与之前介绍的相同。
    举个例子:
    比如要匹配: 2018-12-302018.12.302018/12/30 三种形式。
    可能首先想到的是:\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2},但是:

    var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
    var text = '2018-12.30';
    console.log(regex.test(text));  // true
    

    显然,我们不希望匹配 2018-12.30 ,我们需要前后的分隔符相同:

    var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
    var text1 = '2018-12.30';
    var text2 = '2018-12-30';
    var text3 = '2018/12/30';
    
    console.log(regex.test(text1));  // false
    console.log(regex.test(text2));  // true
    console.log(regex.test(text3));  // true
    

    这里的 \1 就是对前面 (-|\/|\.) 的引用,表达式可视化如下:

    反向引用可视化

    反向引用的二义性:

    在反向引用中,如果编号大于9就会出现二义性,如:\10 是表示第十个捕获分组呢还是表示第一个捕获分组和一个字符 0 呢?
    在一些编程语言中有专门的规定来避免二义性,但是在JavaScript中并没有,JavaScript对于 \10 的处理是:

    1. 如果存在第 10 个捕获分组,则引用对应的分组
    2. 如果不存在,则引用 \1

    如果,在有第 10 个捕获分组的情况下,要匹配 \1 和 字符0 的话,可以使用下面两种方法:

    • 命名分组
    • 再使用括号将 \10 括起来,比如 (\1)0\1(?:0)

    命名分组

    由于按编号引用分组存在一些问题,如:可读性差,不易维护,二义性等。于是出现了命名分组,使用易记忆,易辨别的名字来代替编号。
    注意:命名分组是 ES2017 新特性。

    语法规则如下:

    • 分组:(?<name>)
    • 提取:$<name>
    • 反向引用:\k<name>

    比如,上文的一个例子可以改为:

    var regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
    var text = '2018-12-30';
    var result = text.replace(regex, '$<month>/$<day>/$<year>');
    
    console.log(result);   // 12/30/2018
    

    对于方法 String.prototype.match()RegExp.prototype.exec() 也有了新玩法:

    var regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
    var text = '2018-12-30';
    var matchObj = text.match(regex);
    
    console.log(matchObj.groups);
    // {year: "2018", month: "12", day: "30"}
    

    在匹配结果中,多了 groups 属性,保存了所有命名捕获分组的匹配结果。

    再来看一个反向引用的例子:

    var regex = /\d{4}(?<split>-|\/|\.)\d{2}\k<split>\d{2}/;
    var text = '2018-12-30';
    
    console.log(regex.test(text));  // true
    

    非捕获分组

    括号的功能有“叠加”性。括号可以表示分组,用来构成单个元素;也可以表示多选结构;但同时,也构成了引用分组。
    在仅仅需要标记范围(分组或多选结构)时,正则表达式保存已经匹配的文本会造成不必要的性能浪费。
    这时候我们可以使用 非捕获型括号 (?:...)来限定分组或多选结构的范围:(?:p)(?:p1|p2)。这种只用来限定范围不捕获匹配文本的分组就是 非捕获分组

    非捕获型分组的优点是性能好,缺点是不美观,可读性差。
    在实际应用中,建议尽量使用非捕获分组。

    相关文章

      网友评论

        本文标题:JavaScript 与正则表达式 -- 括号

        本文链接:https://www.haomeiwen.com/subject/ojiblqtx.html