小强赵的个人站点

精进自己,服务他人

RegExp 代码中的代码

正则一直是一块硬骨头,有很多写了几年代码的人依然搞不定这玩意,今天我来换个思路学正则。先给出“正则能做什么”,再传授“解析语法与技巧”,然后给出我收集整理的一些常用正则,最后从“原理与性能”出发对各种正则方案优中选优。

正则能做什么

我们先抛开语法与技巧,看看此兽究竟有什么神通。(此节中只做展示,技术细节会放在下一节分析)

检测格式

这是最常用的一个功能,比如下面判断某字符串是否全部由数字组成。

/^\d+$/.test('0123');   // true
/^\d+$/.test('0123.');   // false

内容替换

这个也比较常用,比如下面将字符串中的数字替换成#号。

'1a2b3c'.replace(/\d/g, '#');   // "#a#b#c"

提取内容

字符串的 match 方法可以将匹配到的字符串片段复制到数据中并返回。

'<div><span>aa</span><span>bb</span></div>'.match(/<span>.*?<\/span>/g);
// ["<span>aa</span>", "<span>bb</span>"]

还有一些更灵活的字符串替换用法,提取特定模式下的部分字符串。

'a11,a12'.replace(/[a-zA-Z]+(\d+)/g, (matchStr, $1) => {
    return $1;
});  // 运行结果:11,12

确定位置

用下面两种方法可以获得被匹配字符串片段的起始和终止位置。

var regexp = /<span>.*?<\/span>/g;
regexp.exec('<div><span>aa</span><span>bb</span></div>');
// 运行结果是 ["<span>aa</span>"] ,regexp.lastIndex 的值是 20(从1数起,第一个span结尾>的位置)
'<div><span>aa</span><span>bb</span></div>'.search(regexp);
// 5 (从0数起,第一个span的<的位置)

分割字符串

一般的字符串分割都是基于固定的分隔符,但是在某些场景下用正则处理会更简单, 比如用空格来分割字符串,如果有两个连续的空格出来的结果可能不是我们期待的, 这时就要用正则来处理,如下例:

'a  b c'.split(' ');    // ["a", "", "b", "c"]
'a  b c'.split(/ +/);   // ["a", "b", "c"]

解析语法与技巧

下面我们将正则的各种语法与技巧溶入实例中来逐项的说明:

新建正则

两种方式等价,注意字符转译,代码如下:

new RegExp('\\d', 'g')
/\d/g

在系统中我们经常需要接收一个正则配置字符串然后返回一个正则,这时你可能需要这样一个函数:

function createRegExpObject(text) {
    var matchedText = text.match(/^\/(.*?)\/([gmiy]*)$/);
    var reg;
    if (!matchedText || !matchedText[1]) {
        return null;
    }
    try {
        reg = new RegExp(matchedText[1], matchedText[2] || '');
    } catch (e) {
        reg = null;
    }
    return reg;
}

简写字符与常用语法

我们以“检测合格”为例来探究匹配的技巧。

\d :匹配数字,相当于 [0-9]

\w :匹配数字字母下划线,相当于 [0-9a-zA-Z_]

\s :任何 Unicode 空白符,可用来匹配空格,Tab,回车

\S :大写的对应其补集

. :除换行外的其他字符

[...] :方括号内的任意字符

[^...] :不在方括号内的任意字符

^ :写在正则开头时只匹配字符串的起始位置

$ :写在正则结尾时只匹配字符串的结尾位置

当不使用 ^$约束时,正则匹配的是局部,否则匹配全部。看下面两个示例:

// 示例一:判断字符串中是否有数字
/\d/.test('a');    // false
/\d/.test('a1a');  // true

// 示例二:判断字符串是否由数字组成
/^\d+$/.test('a1a');    // false
/^\d+$/.test('123');    // true

细心的你可能注意到了示例二中的正则多了一个加号,对于这个加号在下一节中有详解。

另外 .[\s\S] 在正则中是非常有用,常被用来任意字符,区别主要在换行上,如下面例子:

var str = '' 
        + '<p>\np1你好\n</p>'
        + '<p>p2</p>';
var regexp1 = /<p>.*?<\/p>/gi;
var regexp2 = /<p>[\s\S]*?<\/p>/gi;
str.match(regexp1);
// 结果:["<p>p2</p>"]
str.match(regexp2);
// 结果:["<p>\np1你好\n</p>", "<p>p2</p>"]

重复与修饰符

{n, m} :匹配前一项至少n次,但不能超过m次

{n,} :匹配前一项n次或更多次

{n} :匹配前一项n次

? :等价于{0, 1}

* :等价于{0, }

+ :等价于{1, }

再来一个:“判断字符串中是否有3个连续数字”。

/\d{3}/.test('-123a-');    // true
/\d{3}/.test('-12a3-');    // false
/\d{3}/.test('-1234-');    // true

虽然上面都满足了需求,但是最后一个好像有点问题(按照逻辑是讲得通的,4个数字连续当然3个肯定连续),那有没有办法把4个及4个以上连续这种情况排除在外呢?请接着往后看。

再来个难点的需求:“判断某字符串是否全部由数字组成”(其实就是最开始的那个)。

这个需求我们需要换种说法才能动手写正则:“以数字开头以数字结尾且中间没有其他字符”。

要完成上面的正则还需补充说明如下两条语法:

^ :匹配字符串的开头;在多行检索中匹配一行的开头

$ :匹配字符串的结尾;在多行检索中匹配一行的结尾

/^\d+$/.test('0123');   // true
/^\d+$/.test('123.');   // false

上面提到多行检索,下面介绍一下修饰符:

i :执行不区分大小写的匹配

g :执行全局匹配,即找到所有的匹配,而不是找到第一个就停止

m :多行匹配模式,对有换行符的字符串进行多行匹配

多行匹配模式不太好理解,我们紧接着上面的例子给出解释:

/^\d+$/m.test('123.\n123');     // true
// \n是换行符,上面的正则在第一行(\n前面的部分)没有找到匹配的字符串就接着在第二行(\n后面的部分)找
// 并且找到了可以匹配的字符串,所以返回true
/^\d+$/.test('123.\n123');     // false

注:不同的系统和环境可能产生不同的换行,如果需要处理来自较多环境的字符串,可先用下面的方法做格式统一。

str.replace(/\r\n|\r|\u2424/g, '\n');

贪婪

字符串的 match 方法:将匹配到的一个或多个字符串片段放入数组中并返回,如果没有匹配项返回 null

'<div><span>aa</span><span>bb</span></div>'.match(/<span>.*?<\/span>/g);
// ["<span>aa</span>", "<span>bb</span>"]

对于正则的exec 方法,如果正则有修饰符 g(全局匹配),每次提取一部分,然后改变 lastIndex 的值,下次继续从 lastIndex 的地方开始,没有匹配到或者匹配结束返回null,并将 lastIndex 的值置为0。如果正则无修饰符 g(全局匹配),exec 方法只提取匹配到的第一段并返回(没有匹配到返回 null),此时特别注意: lastIndex 的值恒定为0。

var regexp = /<span>.*?<\/span>/g; // 此时 regexp.lastIndex; 的值为0
regexp.exec('<div><span>aa</span><span>bb</span></div>');
// ["<span>aa</span>"]
// 此时 regexp.lastIndex 的值是 20

regexp.exec('<div><span>aa</span><span>bb</span></div>');
// ["<span>bb</span>"]
// 此时 regexp.lastIndex 的值是 35

regexp.exec('<div><span>aa</span><span>bb</span></div>');
// null
// 此时 regexp.lastIndex 的值是 0

贪婪匹配是尽可能多的匹配(不加问号),非贪婪匹配是尽可能少的匹配(加问号),看下面的例子

// 贪婪匹配
'<div><span>aa</span><span>bb</span></div>'.match(/<span>.*<\/span>/g);
// ["<span>aa</span><span>bb</span>"]
// 非贪婪匹配
'<div><span>aa</span><span>bb</span></div>'.match(/<span>.*?<\/span>/g);
// ["<span>aa</span>", "<span>bb</span>"]

非贪婪匹配有一个容易让人误解的地方,就是“尽可能少的匹配”是基于正则原理的“尽可能少”。先看下面例子:

'aaab'.match(/a+?b/);   // ["aaab"]

我们解释一下这种现象:“尽可能少的匹配”可以被理解成只匹配 "ab",但是正则是从左到右的匹配,“a+”匹配1到多个“a”再组合一个“b”,于是就得到了上面结果。在这种“非贪婪 + 固定匹配”模式的匹配中非贪婪部分会呈现出贪婪性,所以上面的正则与下面的正则结果相同。

'aaab'.match(/a+b/);    // ["aaab"]

断言

语法:

(?=p) :零宽正向先行断言,要求接下来的字符串都与 p 匹配,但不能包含匹配 p 的那些字符

(?!p) :零宽负向先行断言,要求接下来的字符串不与 p 匹配

多数的正则书籍或文档都会给出如上的语法解释,尤其是“零宽负向先行断言”这句的翻译就太难理解了, 先看下面的英文原版描述(上下两段内容均出自《JavaScript权威指南》):

(?=p) :A positive lookahead assertion. Require that the following characters match the pattern p, but do not includethose characters in the match.

(?!p) :negative lookahead assertion. Require that the following characters do not match the pattern p.

我解释给大家听:

上面的断言是对写在其之前的正则的约束,即向后约束,也就是要求后面必须有什么(或没什么),如下面正则中括号内的部分是对 bed 做约束,即 bad 之后必须是 room 才能形成匹配。

'bedding'.match(/.+(?=room)/g);    // null
'bedroom'.match(/.+(?=room)/g);    // ["bed"]
'washroom'.match(/.+(?=room)/g);   // ["wash"]

下面是只有 bad 之后不是 room 才能形成匹配

'bedding'.match(/bed(?!room)/g);    // ["bed"]
'bedroom'.match(/bed(?!room)/g);    // null

再补充一点,js不支持向前约束的语法 ?<=?<! ,在 ES6中也没发现对此语法的改进,所以在未来很长一段时间内 js 的正则不会有向前约束。 先说一个只适合部分情况的解决方案吧(这种情况虽然局部但是比较常用):

这种方案只能提取一个匹配,并且不能加修饰符 g,如下面的例子是实现“提取提取字母后的数字”。 如果提取到结果数组的 length 是2,取最后一个就是要提取的数字了。

'a11,a12'.match(/[a-zA-Z]+(\d+)/);  //["a11", "11"]
'11,12'.match(/[a-zA-Z]+(\d+)/);    // null
'a11,a12'.match(/[a-zA-Z]+(\d+)/g); // ["a11", "a12"]

提取示例:

var result = 'a11,a12'.match(/[a-zA-Z]+(\d+)/);    // 如没找到,赋值 null
if (result && result.length === 2) {
    result = result[1];                            // 如找到,赋值字符串
}

选择、分组和引用

选择最容易理解,就是条件“或”的意思,语法上采用 | (竖线),如下示例,正则匹配“字母或数字”

'a1b2c345#-!d67'.match(/\d+|[a-zA-Z]+/g);
// ["a", "1", "b", "2", "c", "345", "d", "67"]
// 语法解释:匹配多个数字 或 多个字母
// 在从左向右的匹配中“数字”和“字母”都是贪婪匹配

分组使正则更为强大,可以处理复杂的“条件”和“重复”等逻辑,

'a1b2c345#-!d67'.match(/(\d+|[a-zA-Z]+)+/g);
// ["a1b2c345", "d67"]
// 语法解释:匹配由数字 和 字母组成的字符串片段

引用是一个妙用无穷的语法,由括号的使用引起,在正则中使用的格式是 \数字,在字符串的 replace 方法中以参数的形式出现,引用从1算起,以左括号作为计数参照;引用的是前面模式匹配到的文本的引用;另外用 (?: 代替左括号时次对括号不计入引用序列。上面这段话不好理解,下面用示例来解释:

先看一个“匹配最内层引号(单引号和双引号)及其中内容”的正则:

'"a\'b\'123"'.match(/(['"])[^'"]*\1/g);
//["'b'"]

首先说明一下字符串中的 \' 是应为字符串是以单引号创建的,直接使用单引号会引起语法错误,所以需要用转译的形式引入,并且 \' 是一个字符。

然后是过程:

第一步从第一个双引号开始匹配,与第一个中括号匹配,继续向前;

第二步到 a 符合正则中第二个中括号(匹配非引号)与其重复限制符 *的匹配逻辑;

第三步第二个中括号依然在起作用,这时单引号不符合中括号的模式要求,所以中括号的匹配范围结束,\1的匹配开始(明星登场了),将上面那句话放进来“引用的是前面模式匹配到的文本的引用”,也就是第一步匹配到的是双引号,所以此时是用单引号匹配双引号,匹配失败,跳出此次匹配;(*是匹配0到多个,所以第二个中括号匹配完 a 继续尝试匹配单引号)

第四步从 a 开始,遇到第一个中括号就匹配失败,跳出再继续;

第五步第一个单引号与第一个中括号匹配成功,继续向前,b 与第二个中括号匹配,继续向前,第二个单引号与中括号匹配失败,中括号范围结束,第二个单引号继续向前匹配,此时的 \1 是前面匹配到的单引号,所以单引号匹配单引号,成功,返回字符串 'b'

第六步从 b开始匹配,之后的都会失败,不再叙述。

再看一个引用在字符串替换中的例子:

'a11,a12'.replace(/([a-zA-Z]+)(\d+)/g, function (matchStr, $1, $2) {
    return $2 + $1;
});// "11a,12a"

上面是匹配字母和数字的有序单一组合 (像前面一段是字母后面一段是数字并且没有其他类型字符参与的 这种情况), 我们利用回调函数来交换字母段与数字段的顺序,还有一种更简单的写法:

'a11,a12'.replace(/([a-zA-Z]+)(\d+)/g, '$2$1');
// "11a,12a"
'a11,a12'.replace(/(?:[a-zA-Z]+)(\d+)/g, '$2$1');
// "$211,$212"可以看到排除引用语法(?:的作用,如果没有第二个引用$2被当做普通的字符串处理

最后说一下 [],中括号中的小括号和加号这些特殊字符都变成了普通字符,

"(d1+23".replace(/[(\d)+]/g, '#')     // "#d####"

来个例子串一串

三个连续数字,多一个不行少一个也不行。

格式检测

js不支持向前约束(专业术语叫“前瞻”),在《高性能javascript》中提到一种变通的方法: (?=(patten) to make atomic)\1 。这种方法的使用如下:

/(?!\d)[^\1]\d{3}(?!\d)/g.test('-1234--567');
// true
/(?!\d)[^\1]\d{3}(?!\d)/g.test('-1234--5678');
// false

内容替换

如果直接将上面的正则用于内容替换就会出问题了,向前约束会多匹配一个字符,如下:

'-1234--567'.replace(/(?!\d)[^\1]\d{3}(?!\d)/g, '###');
// "-1234-###"

由于正则 js 前瞻功能的缺失,在“前后固定替换中间”的这种问题中会将前瞻部分也匹配进来。好在 replace 的第二个参数支持回调函数,配合分组(注意:(\d{3}) 是加了括号的),可以将真正要匹配的内容拿出来再做处理,最终的解决方案如下:

'-1234--567'.replace(/(?!\d)[^\1](\d{3})(?!\d)/g, function ($1, $2, position, str) {
    // $1 "-567"
    // $2 "567"
    // position 6
    // str "-1234--567"
    return $1.replace($2, '###');
});

提取内容

提取内容和上面内容替换会遇到同样的问题,但解决方案要简单一些:

'-1234--567'.match(/(?!\d)[^\1](\d{3})(?!\d)/);
// ["-567", "567"]

会将分组内容写在后面,加一点判断会更好

var matchResult = '-1234--567'.match(/(?!\d)[^\1](\d{3})(?!\d)/);
if (matchResult !== null) {
    matchResult[1];
    // "567",输出还是赋值就看你的代码逻辑了
}

确定位置

确定位置可以通过 string.replace,也可通过 string.search。

string.replace 通过回调向外赋值:

var p;
'-1234--567'.replace(/(?!\d)[^\1](\d{3})(?!\d)/, function ($1, $2, position, str) {
    // $1 "-567"
    // $2 "567"
    // position 6
    // str "-1234--567"
    p = position + $1.replace($2, '').length;
});
// p = 7;

string.search,前瞻部分最好是固定长度的:

'-1234--567'.search(/(?!\d)[^\1]\d{3}(?!\d)/) + 1
// 7

分割字符串

字符串分割没有办法直接绕过js前瞻缺失造成的影响:

'-1234--567--890'.split(/(?!\d)[^\1]\d{3}(?!\d)/)
// ["-1234-", "-", ""]

可以通过 replace 先替换再分割的方式来完成:

var space = '###';
'-1234--567-789'.replace(/(?!\d)[^\1](\d{3})(?!\d)/g, function ($1, $2, position, str) {
    // $1 "-567"
    // $2 "567"
    // position 6
    // str "-1234--567"
    return $1.replace($2, space);
}).split(space);
// ["-1234--", "-", ""]

常用正则

因为理论讲多了看完也忘的差不多了,所以我们直接进入实践,这里的例子都是我工作中收集的,也还将继续收集下去,当然欢迎补充和指正。 有些例子会有方案的进化,这就涉及到原理和优化技巧了,如果对其中提到的东西有疑问可以去下一节看看。

正则的语法与技巧就如上面所言了,编写正则还有很重要的一环就是翻译, 比如有个需求是“匹配电话号码”,那么就要先细化什么样的字符串才能算是电话号码, 11位数字?那以0或9开头可以吗?10位的400电话算吗?极端一点110是电话号码吗? 我在这里给不出标准答案,只有结合业务场景才能做出合理的取舍。

其实翻译分为两个阶段,第一个阶段是范围的确定,上面一段说的就是此段, 第二个阶段是语法的翻译阶段,这一阶段考验对语法的熟练程度和使用技巧, 如上节提到的“字符串是否全部由数字组成”,翻译成“以数字开始,以数字结尾,且中间无其他字符”。 这一节会给出很多日常开发中能用到实例,先看第一个:

QQ号

QQ最小号10000,这个容易查到,但最多几位呢?10位!这是我找到的十位账号:2,723,765,184。 所以正则就简单了:

/^[1-9]\d{4,9}$/.test('2723765184');    // true
// 下面是网上常见的错误答案
/[1-9]\d{4,}/        // 这个错的很离谱,这些都能匹配成功:abc12345,123456789012345
/^[1-9]\d{4,}$/      // 这个已经和接近正确了,只是他能匹配11位即以上的QQ号,可能作者不能确定QQ最多几位吧

可以把上面正则中的 “9”改成 “10” 来支持11位的QQ号,以兼容未来可能出11位的QQ号。

PS:10000是腾讯公司的专线QQ,10001是马化腾的,10002是腾讯二号人物张志东。

邮箱

正则只能检测邮箱的格式,不能检测邮箱是否存在, 网上有人问“1234jh@sb126.com”这种邮箱也能匹配成功很不能理解,于是大呼“有没有靠谱的邮箱正则”, 解释一下:对于“@sb126.com”这一段是邮箱服务提供商确定的,如果你想恶心网易完全可以搭建一套邮箱系统提供一个这样的邮箱服务; 对于前半段“1234jh”是邮箱名,各家邮箱的要求不一样, QQ邮箱全部是数字,网易的要求是“6~18个字符,可使用字母、数字、下划线,需以字母开头”, 部分邮箱容许有英文句号出现, 老外的邮箱(如gmail)支持“sky”这样的简短邮箱名。

所以邮箱名部分我们抽象一下就是“字母、数字、下划线、英文句号组成,长度最小为1最大不限”; “@”字符是必须的,没什么好说的,“@”之后的就用“.”分成两部分,而且只有两部分; 第一部分是多个数字或字母,所以我们用 \w+ 作为匹配,第二部分一般是国际域名(只有字母)我们用 [a-z]+ 匹配。

/^[\w\.]+@\w+\.[a-z]+/.test('nihao@163.com');     // true
/^[\w\.]+@\w+\.[a-z]+/.test('nihao@vip.qq.com');  // true

另外如果用户向提供一个假邮箱伪造是非常容易的,如果客户的邮箱对业务比较重要, 一般采用邮件链接激活账户的方式而不是拒绝用户提供假邮箱。

匹配身份证

先描述一下身份证的规则

15位身份证号码
130503    670401    001
6位地区    6位生日    3位顺序号    (15位身份证号无验证位)

18位身份证号码
130184    19860305    106                  x
150105    19871126    361                  1
6为地区    8位生日      顺序码(未数男单女双)    校验码

关于地区编码可以从 这里 查到,最小值北京(110000)最大值澳门特别行政区(820000)。

由于二代身份证已经全国普及,所以如果不是做档案之类的向前收集数据的系统15位身份证号码基本可以忽略, 所以我们这里只讨论18位身份证。

先说前6位地区编码,由于编码不连续且数据量大所以无法穷举,只能给一个大致的范围, 这其中的编码肯定有一部分不符合国家地区编码却可以通过验证,如下:

[1-8]\d{5}   // [1-8]由地区编码的最小值和最大值决定

再看8位生日

[1-2]\d{3}          // 匹配年份,其中[1-2]是年份的第一位,当然2999也会匹配成功
((0\d)|(1[0-2]))    // 匹配月份
(([0-2]\d)|3[0-1])  // 匹配日,
// 当然 0230 这样的日期将会匹配成功(这当然不是我们期待的,但是正则只能匹配格式对数据逻辑无能为力)

3位顺序码

\d{3}

1位验证码

[\d|X|x]    // X的大小写支持更具业务情况确定,建议大小写都支持然后统一装换成大写

将前面的合起来,再加上起始和结尾约束,最终的正则示例如下:

/^[1-8]\d{5}[1-2]\d{3}((0\d)|(1[0-2]))(([0-2]\d)|3[0-1])\d{3}[\d|X]$/.test('13018419860305106X');
// true

另外还有一种加权计算方法,最后一位验证码就是通过前17位加权计算得出, 具体点击此链接, 绕过加权计算的验证成本比较大。

匹配ip地址

/\d+\.\d+\.\d+\.\d+/

去前后空格

.replace(/(^\s*)|(\s*$)/g, '');

匹配浮点数

先稍微简化一点:“匹配正浮点数”

我们先进行第一阶段的翻译,即确定范围: 十进制的浮点数,1,0.1是合法的, 另外确定001也是合法的,因为001参与运算时和1相同且这种用户输入形式比较边缘, 1.和.1是非法的

再进行第二阶段翻译,语法翻译: 以数字开头,并且数字不少于一个,后面一个点+多数字的组合形式 以零到多个结尾

/^\d+(\.\d+)?$/.test('234.3');       // true
/^\d+(\.\d+)?$/.test('234.3.2');     // false

再加上正负号的匹配规则就是“匹配浮点数”了

/^[+-]?\d+(\.\d+)?$/.test('001');    // true
/^[+-]?\d+(\.\d+)?$/.test('-0.01');  // true

金额验证

/^(0|([1-9]\d*))(\.\d{1,2})?$/.test('012');   // false
/^(0|([1-9]\d*))(\.\d{1,2})?$/.test('.1');    // false
/^(0|([1-9]\d*))(\.\d{1,2})?$/.test('1.');    // false
/^(0|([1-9]\d*))(\.\d{1,2})?$/.test('1.1.1'); // false
/^(0|([1-9]\d*))(\.\d{1,2})?$/.test('+1');    // false
/^(0|([1-9]\d*))(\.\d{1,2})?$/.test('12');    // true
/^(0|([1-9]\d*))(\.\d{1,2})?$/.test('12.12')  // true
/^(0|([1-9]\d*))(\.\d{1,2})?$/.test('0.12')   // true

中文

硬知识,只能匹配中文,中文标点不被匹配

/[\u4e00-\u9fa5]/.test('你');    // true
/[\u4e00-\u9fa5]/.test('。');    // false

再补充一个双字节的正则,可以用这个计算字符串的宽度

/[^\x00-\xff]/.test('你');   // true
/[^\x00-\xff]/.test('。');   // true
var str = 'abc你好';
var matchArr = str.match(/[^\x00-\xff]/g);
str.length + (matchArr === null ? 0 : matchArr.length);   // 7

匹配静态资源

范围:html页面,css和js文件,png图片和ico图标。支持问号来处理浏览器资源缓存。

语法:以 .扩展名 结尾,如果有问号则问号及问号后的东西不参与结尾的计算。

/\.+[html|css|js|png|ico]+(\?.*)?$/ig.test('dfsdf.html?e');    // true
/\.+[html|css|js|png|ico]+(\?.*)?$/ig.test('dfsdf.html.xml?e');// false

以上两个正则用到的 $ 约束技巧很常见, 如果不加此约束像 “dfsdf.html.xml?e”这样的资源就可以被被匹配成功。 另外可有可无的那部分是否可以重复出现也很关键, 匹配浮点数时“.数字”不可以出现多次,但是匹配静态资源时“?任意字符串”是可以重复初出现的。

正则分割千分位

'-12345678.90'.replace(/\d(?=(\d{3})+(?:$|\.))/g,"$&,”);     // 作者:张云龙
// 如果确定没有小数点,那么下面的正则会更简单
"1234567".replace(/\B(?=(?:\d{3})+$)/g, ',');                // 作者:李永剑

这里需要解释一下 ?:,不想被捕获的时候使用 可以提高程序执行速度,比如

([a-z][0-9])+

这个正则表达式里 ( ) 里面的内容被捕获了,反向引用的时候可以用上。但是如果写成

(?:[a-z][0-9])+

跟上面 正则表达式 整体匹配是一样的 就是 不会捕获 ( )里内容了。也就是不能使用反向引用,如果还是不太理解, 那就先了解一下 反向引用吧。

提取文本中的图片地址

'"https://a.b.cc","https://a.b.c/s902.jpg","https://a.b.c/s03.jpg"'.match(/(https:\/\/){1,1}[^"]*?(\.png|\.jpg){1,1}/g);
// 结果
[
    "https://a.b.c/s902.jpg"
    "https://a.b.c/s03.jpg"
]

这里比较巧妙的用到了双引号,如果字符串中的图片地址左右没有双引号,那么此方法是有问题的。

原理与性能

对任何技术的原理有深入理解都会有助于应用,但是性能方面个人偏向于在需要的时候在优化,毕竟机器越来越便宜人越来越贵。

原理与原则

回溯是正则美好与强大的根源,但回溯计算代价较高,所以减少回溯是正则优化的关键。

另外正则匹配所消耗的时间与资源与所要处理的字符串有很大关系,所以为了得到更好的结果你需要在各种字符串上进行测试,包括能匹配的,不能匹配的的和近似匹配的,在方案的取舍上我不喜欢在某种情况下很好却在另外一种情况下很糟糕的方案,而喜欢在任何情况下都表现稳定,可能这种方案不是最好的,但在其出现的所有场景中都不会糟糕到影响用户使用。

优化技巧

cat|bat 替换 [cb]at,用 red|read 替换 rea?d,用 red|raw 替换 r(?:ed|aw)。最后一个替换对于替换和提取有副作用。

跟进

js 还是一门处于发展中的语言,ES6还在发展中,截止到目前(2016.01)处于公示版是ECMA-262阮一峰的博客对这个的描述很到位。如果想了解从草案到标准的过程可以参看相对应的草案

参考

w3school的regexp部分

ECMA-262号官方文档

https://developer.mozilla.org/RegExp

可视化正则

阮一峰正则 es6