《Javascript正则表达式迷你书》学习笔记(一)

812 阅读7分钟

最近看了老姚的《Javascript正则表达式迷你书》,对正则表达式的理解和应用有了些新的收获。一般遇到一些复杂些的表达式都会选择百度解决,但是当你理解了正则表达式的组成和原理后发现自己实现也没那么困难。

先提供一个辅助理解正则表达式的在线工具:jex.im/regulex

1. 正则基础

什么是正则表达式?

正则表达式(英语:Regular Expression,常简写为regex、regexp或RE),又称正则表示式、正则表示法、规则表达式、常规表示法,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。(维基百科)

语法

// 两种形式
var reg = new RegExp(pattern, modifiers)
或者
var reg = /pattern/modifiers

正则表达式分为pattern(模式匹配串主体)和modifiers(修饰符)两部分。后面我们主要讲pattern,这里先介绍下modifers

修饰符

  • i-ignoreCase 匹配不区分大小写
  • m-multiline 多行匹配,用于匹配存在换行符的字符串
  • g-globalMatch 全局匹配,不是匹配到第一个就停止匹配
var reg = /gakki/img
'Gakki\ngakki'.match(reg) // ["Gakki", "gakki"]

'Gakki\ngakki'.replace(/^|$/g, '#')
'Gakki\ngakki'.replace(/^|$/mg, '#')

正则表达式是匹配模式,要么匹配字符,要么匹配位置

2. 正则表达式字符匹配攻略

两种模糊匹配

  • 横向模糊匹配(一个正则可匹配的字符串的长度不是固定的)
    var regex = /ab{2,5}c/g;
    var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
    console.log( string.match(regex) );
    // => ["abbc", "abbbc", "abbbbc", "abbbbbc"]

  • 纵向模糊匹配 (一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种 可能)
    var regex = /a[123]b/g;
    var string = "a0b a1b a2b a3b a4b";
    console.log( string.match(regex) );
    // => ["a1b", "a2b", "a3b"]

字符组

例如 [abc],表示匹配一个字符,它可以是 "a"、"b"、"c" 之一。

  • 范围表示法
/[123456abcdefGHIJKLM]/ 等价于 /[1-6a-fG-M]/

问题:要匹配'a', '-', 'z'这三个字符之一的正则怎么写?

  • 排除字符组

可以是任意字符但不是[123456abcdefGHIJKLM]中的任何一个字符

/[^123456abcdefGHIJKLM]/ 等价于 /[^1-6a-fG-M]/

^脱字符,表示求反

  • 常见的简写形式
字符组 具体含义
\d 表示 [0-9]。表示是一位数字。 记忆方式:其英文是 digit(数字)。
\D 表示 [^0-9]。表示除数字外的任意字符。
\w 表示 [0-9a-zA-Z_]。表示数字、大小写字母和下划线。 记忆方式:w 是 word 的简写,也称单词字符。
\W 表示 [^0-9a-zA-Z_]。非单词字符。
\s 表示 [ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页 符。记忆方式:s 是 space 的首字母,空白符的单词是 white space。
\S 表示 [^ \t\v\n\r\f]。 非空白符。
. 表示 [^\n\r\u2028\u2029]。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符 除外。记忆方式:想想省略号 ... 中的每个点,都可以理解成占位符,表示任何类似的东西。

问题表示任意字符怎么写?

量词

量词 具体含义
{m,} 表示至少出现 m 次。
{m} 等价于 {m,m},表示出现 m 次。
? 等价于 {0,1},表示出现或者不出现。 记忆方式:问号的意思表示,有吗?
+ 等价于 {1,},表示出现至少一次。记忆方式:加号是追加的意思,得先有一个,然后才考虑追加。
* 等价于 {0,},表示出现任意次,有可能不出现。
var regex = /a{1,}b{2}c?d+e*/
regex.test('aabbde') // true
regex.test('abbce') // false

  • 贪婪匹配和惰性匹配

贪婪匹配例子

    var regex = /\d{2,5}/g;
    var string = "123 1234 12345 123456";
    console.log( string.match(regex) );

惰性匹配例子

    var regex = /\d{2,5}?/g;
    var string = "123 1234 12345 123456";
    console.log( string.match(regex) );

惰性匹配的记忆方式:量词后面加个问号?,问一问你知足了吗,你很贪婪吗

多选分支

一个模式可以实现横向和纵向模糊匹配,而多选分支可以支持多个子模式任选其一。 具体形式如下:(p1|p2|p3),其中 p1、p2 和 p3 是子模式,用 |(管道符)分隔,表示其中任何之一。

匹配字符串 "good" 和 "nice" 可以使用 /good|nice/

    var regex = /good|nice/g;
    var string = "good idea, nice try.";
    console.log( string.match(regex) );
    // => ["good", "nice"]

问题:用 /good|goodbye/,去匹配 "goodbye" 字符串时,结果是?

案例分析

  • 匹配十六进制色值
    #ffbbad
    #Fc01DF
    #FFF
    #ffE
  • 匹配日期
    2017-06-10 20:13
  • 匹配元素
    <div id="container" class="main"></div> 中提取出 id="container"

3. 正则表达式位置匹配攻略

什么是位置(锚)

表示相邻字符间的位置

'hello' = '' + 'h' + '' + 'e' + '' + 'l' + '' + 'l' + '' + 'o' + ''

匹配字符的方式

ES5 中有六个锚: ^、$、\b、\B、(?=p)、(?!p)

var regex = /^$\b\B(?=a)(?!b)/

^ 和 $

^(脱字符)匹配开头,在多行匹配中匹配行开头

$(美元符号)匹配结尾,在多行匹配中匹配行结尾

  • 在'hello world' 前后插入一个#
var str = 'hello world'
str.replace(/^|$/g, '#')
  • 在'hellow\nworld'每行的开头结尾插入一个#如何实现?

\b 和 \B

\b 是单词边界,具体就是 \w 与 \W 之间的位置,也包括 \w 与 ^ 之间的位置,和 \w 与 $ 之间的位置

    var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
    console.log(result);
    // => "[#JS#] #Lesson_01#.#mp4#"

\B 就是 \b 的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉 \b,剩下的都是 \B 的。 具体说来就是 \w 与 \w、 \W 与 \W、^ 与 \W,\W 与 $ 之间的位置

    var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
    console.log(result);
    // => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"

(?=p) 和 (?!p)

(?=p),其中 p 是一个子模式,即 p 前面的位置,或者说,该位置后面的字符要匹配 p

    var result = "hello".replace(/(?=l)/g, '#');
    console.log(result);
    // => "he#l#lo"

(?!p) 就是 (?=p) 的反面意思

    var result = "hello".replace(/(?!l)/g, '#');
    console.log(result);
    // => "#h#ell#o#"

案例分析

  • 不匹配任何东西的正则
/.^/
  • 数字的千位分隔符表示法

比如把 "12345678",变成 "12,345,678"。以下写法对吗?

    var result = "12345678".replace(/(?=(\d{3})+$)/g, ',')
    console.log(result);
  • 验证密码

密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符

第一步:不考虑“至少包括两种字符”

var regex = /^[0-9a-zA-Z]{6,12}$/

第二步:判断是否包含有某一种字符

var regex = /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/;

第三步:同时包含具体两种字符

var regex = /(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/;

第四步: 题目分解

同时包含数字和小写字母

同时包含数字和大写字母

同时包含小写字母和大写字母 同时包含数字、小写字母和大写字母

以上的 4 种情况是或的关系

var regex = /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A- Z]))^[0-9A-Za-z]{6,12}$/;
console.log( regex.test("1234567") ); // false 全是数字
console.log( regex.test("abcdef") ); // false 全是小写字母
console.log( regex.test("ABCDEFGH") ); // false 全是大写字母 
console.log( regex.test("ab23C") ); // false 不足6位 
console.log( regex.test("ABCDEF234") ); // true 大写字母和数字 
console.log( regex.test("abcdEF234") ); // true 三者都有

另一种解法

“至少包含两种字符”的意思就是说,不能全部都是数字,也不能全部都是小写字母,也不能全部都是大写字母。利用(?!p)来实现

var regex = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/; 
console.log( regex.test("1234567") ); // false 全是数字
console.log( regex.test("abcdef") ); // false 全是小写字母
console.log( regex.test("ABCDEFGH") ); // false 全是大写字母
console.log( regex.test("ab23C") ); // false 不足6位 
console.log( regex.test("ABCDEF234") ); // true 大写字母和数字 
console.log( regex.test("abcdEF234") ); // true 三者都有

4. 正则表达式括号的作用

分组和分支结构

    var regex = /(ab)+/g;
    var string = "ababa abbb ababab";
    console.log( string.match(regex) );
    // => ["abab", "ab", "ababab"]
    
    var regex = /^I love (JavaScript|Regular Expression)$/;
    console.log( regex.test("I love JavaScript") );
    console.log( regex.test("I love Regular Expression") );
    // => true
    // => true

分组引用

通过括号的使用,我们可以进下数据提取,数据替换的操作

  • 日期 yyyy-mm-dd

  • 日期 yyyy-mm-dd 加上括号分组后

正则引擎在匹配的过程中会给每一个group分配一个存储空间来存储匹配到的数据

  • 提取 yyyy-mm-dd 的年月日
    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    var string = "2017-06-12";
    console.log( string.match(regex) );
    // => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    var string = "2017-06-12";
    regex.test(string); // 正则操作即可,例如 
    //regex.exec(string); 
    //string.match(regex);
    console.log(RegExp.$1); // "2017"
    console.log(RegExp.$2); // "06"
    console.log(RegExp.$3); // "12"

match 返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的 NOTE 内容,然后是匹配下标,最后是输入的文本。另外,正则表达式是否有修饰符 g,match 返回的数组格式是不一样的(和正则对象的exec方法一样)

  • yyyy-mm-dd 替换为 mm/dd/yyyy
    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    var string = '2017-06-12';
    var result = string.replace(regex, '$2/$3/$1');
    console.log(result);

等价于

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    var string = "2017-06-12";
    var result = string.replace(regex, function () {
        return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
    });
    console.log(result);
    // => "06/12/2017"

也等价于

    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    var string = "2017-06-12";
    var result = string.replace(regex, function (match, year, month, day) {
      return month + "/" + day + "/" + year;
    });
    console.log(result);
    // => "06/12/2017"

RegExp 是javascript中的一个内置对象。为正则表达式。

RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串,以此类推,RegExp.$2,RegExp.$3,..RegExp.$99总共可以有99个匹配

如果你直接在控制台打印RegExp, 出现的一定是一个空字符串: ""。那么, 我们在什么时候可以使用RegExp.$1呢?

其实RegExp这个对象会在我们调用了正则表达式的方法后, 自动将最近一次的结果保存在里面, 所以如果我们在使用正则表达式时, 有用到分组, 那么就可以直接在调用完以后直接使用RegExp.$xx来使用捕获到的分组内容。

反向引用

除了使用相应 API 来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用

  • 一个匹配以下三种格式的正则
2016-06-12
2016/06/12
2016.06.12
    var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
    var string1 = "2017-06-12";
    var string2 = "2017/06/12";
    var string3 = "2017.06.12";
    var string4 = "2016-06/12";
    console.log( regex.test(string1) ); // true
    console.log( regex.test(string2) ); // true
    console.log( regex.test(string3) ); // true
    console.log( regex.test(string4) ); // true
  • 如果要求分隔符前后一致
    var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
    var string1 = "2017-06-12";
    var string2 = "2017/06/12";
    var string3 = "2017.06.12";
    var string4 = "2016-06/12";
    console.log( regex.test(string1) ); // true
    console.log( regex.test(string2) ); // true
    console.log( regex.test(string3) ); // true
    console.log( regex.test(string4) ); // false

  • 括号嵌套的分组
    var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
    var string = "1231231233";
    console.log( regex.test(string) ); // true

这个时候的 RegExp.$1 RegExp.$2 RegExp.$3 RegExp.$4 是什么?

  • \10 代表 第10个分组 还是 \1 和 0?
  • 引用不存在的分组会怎样
    var regex = /(1)\1\2/
    var string = '112'
    regex.test(string) // true or false
  • 分组后有量词最终捕获到的数据是最后一次的匹配
    var regex = /(\d)+/;
    var string = "12345";
    console.log( string.match(regex) );
    // => ["12345", "5", index: 0, input: "12345"]
    
    var regex = /(\d)+ \1/;
    console.log( regex.test("12345 1") );
    // => false
    console.log( regex.test("12345 5") );
    // => true

非捕获括号

之前文中出现的括号,都会捕获它们匹配到的数据,以便后续引用,因此也称它们是捕获型分组和捕获型分支 如果只想要括号最原始的功能,但不会引用它,即,既不在API里引用,也不在正则里反向引用。此时可以使用非捕获括号 (?:p) 和 (?:p1|p2|p3)

    var regex = /(?:ab)+/g;
    var string = "ababa abbb ababab";
    console.log( string.match(regex) );
    // => ["abab", "ab", "ababab"]
    
    var regex = /^I love (?:JavaScript|Regular Expression)$/;
    console.log( regex.test("I love JavaScript") );
    console.log( regex.test("I love Regular Expression") );
    // => true
    // => true

案例分析

  • trim 方法
    function trim(str) {
        return str.replace(/^\s+|\s+$/g, '');
    }
    console.log(trim("foobar "));
  • php 变量驼峰化
    function camelize (str) {
         return str.replace(/[-_\s]+(.)?/g, function (match, c) {
            return c ? c.toUpperCase() : '';
        });
     }
    camelize('enable_create_permant_class')
    // => "enableCreatePermantClass"