JavaScript Regular Expression

May 05, 2014 by sylvenas

ECMAScript3 开始支持正则表达式,其语法和其他语言的正则表达式语法很类似,一个完整的正则表达式结构如下:

var expression = /pattern/flags ;

其中,模式(pattern)部分可以是任何简单或复杂的正则表达式,可以包含字符型、限定符、分组、向前查找以及反向查找。

每个正则表达式都可带有亦或多个标志(flags),用以标明正则表达式的行为,正则表达式支持下列3个标志:

  • g - 表示全局(global)模式,即模式将被应用到所有的字符串,而非在发现第一个匹配项时立即停止。
  • i — 表示不区分大小写(case-insensitive),即在确定匹配项时忽略模式与字符串的大小写。
  • m - 表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项,例如:字符串模板中出现的多行字符串。

如果有多个标志同时使用时,则写成:gmi

正则表达式的创建

正则表达式的创建有两种方式: new RegExp(expression) 和 直接字面量。

// 使用直接字面量创建
var exp1 = /(^\s+)|(\s+$)/g;
// 使用RegExp对象创建
var exp2 = new RegExp('(^\\s+)|(\\s+$)', 'g');

exp1exp2是两个完全等价的正则表达式,需要注意的是,传递给RegExp构造函数的两个参数都是字符串,不能把正则表达式字面量传递给RegExp构造函数。

学习正则表达式的网站:https://regexper.com,可以查看任意的正则表达式的含义

原义文本字符与元字符

原义文本字符

就是代表文本原本含义的字符,例如:abc就是表示文本abc,123就是表示数字123;

元字符

元字符是在正则表达式中有特殊含义的非字母字符

与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义(使用\转义)。正则表达式中的元字符包括: ( )[ ]{ }\^|?*+.$ 后面会详细介绍这些元字符的含义。

字符类

使用元字符[]来构建一个简单的类,所谓类就是指符合某些特征的对象,是一个泛指,而不是特指某个字符,例如:[abc]表示只要有a,b,c中的任意一个就ok!

字符类取反:使用^创建反向类,也就是不属于某个类的内容,就是一个数学取反的操作。例如:[^abc]表示任意不是a,b,c的字符。

范围类:可以使用[a-z]来表示从a到z的任意字符(包含a和z本身);[0-9]表示任意的数字;类内部连写:[a-zA-Z]表示任意的a-Z的大小写字母;

-并不是元字符,如果范围类中使用-可以直接写,例如:2014-05-06,如果要匹配数字和-,/[0-9]-/就可以了

正则表达式默认的字符类

  • . - 等价于[^\r\n] 除了回车和换行符之外的所有字符
  • \d - 等价于[0-9] 数字(digit)字符
  • \D - 等价于[^0-9] 非数字字符
  • \s - 等价于[\t\n\x0b\f\r] 空白符(space)
  • \S - 等价于[^\t\n\x0b\f\r] 非空白符
  • \w - 等价于[a-zA-Z_0-9] 单词(word)字符(字母、数字、下划线)
  • \W - 等价于[a-zA-Z_0-9] 非单词字符

边界

  • ^ - 以x开头的(^在字符类中表示取反)
  • $ - 以x结束
  • \b - 单词边界
  • \B - 非单词边界

量词

  • ? - 出现0次或者一次(最多出现一次)
  • + - 出现一次或者多次(至少出现一次)
  • * - 出现0次或者多次(任意次)
  • {n} - 出现n次
  • {n,m} - 出现n-m次
  • {n,} - 出现至少n次

量词只能作用于紧挨着的字符,而不能作用于整个单词,例如:/mello{5}/,表示的是o重复5次,而绝不是单词mello重复5次。

贪婪模式与非贪婪模式

如果我们有一个字符串12345678,那么正则表达式/\d{3,6}/该如何匹配呢?因为字符串即满足数字出现3次,也满足出现6次,甚至满足出现4次,5次;

而JavaScript是默认的贪婪模式,也就是尽可能多的匹配,也就是上面的表达式会匹配12345678中的123456

如果我们想让正则表达式尽量少的匹配,也就是说一旦匹配成功不再继续尝试,也就是非贪婪模式,只要在量词后面加上?就可以将贪婪模式转换为非贪婪模式。例如上面的正则表达式修改为/\d{3,6}?/,那么这个时候匹配的结果就是123456

分组

上面在量词一节中提到的量词只能作用于紧挨着的字符吗,而不是作用于整个单词,如果我们就是想要匹配整个单词怎么办呢?我们可以使用()来分组。例如:/(mello){5}/就是表示单词mello重复5次

| 表示或的关系,举个例子就是/hello (mello|tyran)/表示能匹配hello 后面跟着mello或者tyran的字符串。

反向引用

前面讲到的都是用各种法则去匹配字符串,那么我们如何捕获我们匹配的内容呢?

举例来说:2014-05-06如果要转换为2014/05/06那么我们可以使用'2014-05-06'.replace(/(\d{4})-(\d{2})-(\d{2})/,'$2/$3/$1')来完成这项工作,其中第二个参数字符串中的,$2$3$1分别表示分组匹配中的第一项,第二项,第三项

忽略分组 如果要取消某个分组的捕获,可以在分组的最开始加上?:即可!

前瞻

正则表达式从文本的头部向尾部开始解析,文本尾部方向,被称为 ,前瞻就是在正则表达式匹配到规则的时候,向前检查是否符合断言

前瞻分为正向前瞻(?=)和负向(?!)前瞻,也就是断言部分可以设定为符合条件和不符合条件。

正向前瞻:exp(?=assert),负向前瞻:exp(?!assert)

例如:'a2*3'.replace(/\w(?=\d)/,'X')的结果为'X2*3',就是正则表达式匹配的部分被替换掉了,而断言部分的没有被替换。

正则表达式对象的属性和方法

属性

var reg = /\d/;

  • reg.global:是否全文检索,默认是false
  • ref.ignoreCase:是否大小写敏感,默认是false
  • multiline:是否是多行检索,默认是false
  • lastIndex:当前表达式匹配内容的最后一个字符的下一个位置(下一次正则表达式开始匹配的字符串的位置)
  • source:正则表达式的文本字符串(本例中为'\d')

RegExp.prototype.test(str)

用于测试字符串参数中是否包含存在匹配正则表达式模式的字符串,如果存在则返回true,否则返回false;

var reg1 = /\w/;
var reg2 = /\w/g;
reg1.test('ab') //true reg2.lastIndex 0
reg1.test('ab') //true reg2.lastIndex 0
reg1.test('ab') //true reg2.lastIndex 0

reg2.test('ab') //true reg2.lastIndex 1
reg2.test('ab') //true reg2.lastIndex 2
reg2.test('ab') //false reg2.lastIndex 0
reg2.test('ab') //true reg2.lastIndex 1

上面的代码中,我们发现当正则表达式有'g'和没有'g'的时候,test函数的表现大不相同

  • 没有g的时候,正则表达式每次匹配字符串的时候不会修改本身的lastIndex属性,也就是每次都是从头开始匹配,那么匹配结果肯定每次都一样。
  • 在有g的时候,每次调用正则表达式的test方式之后,都会修改本身的lastIndex属性,也就是每次开始匹配的字符串的位置都不一样,那么就会导致匹配的结果不一致,也就是上面的例子中第三次调用test的时候的结果变成了false,同时lastIndex属性重置为了0(当匹配不到的时候就会重置为0)。

test方法的本意是看看字符串中是否含有能匹配的模式,不管能匹配几次,也不管在哪里匹配到,所以一般情况下都不会加g,但我们也要知道加g会产生的后果

RegExp.prototype.exec(str)

exec方法会对字符串进行搜索,并且会更新正则表达式本身的lastIndex属性以反应匹配结果,如果没有匹配的结果则返回null,否则返回一个结果数组(该数组包含两个特殊的属性,index表示声明匹配文本的第一个字符的位置,input存放被检索的字符串string)。

exac在有g和没有g的时候表现也不一样。

  • 没有g - 类似于test没有g的时候,每次执行exac的时候,正则表达式本身的lastIndex不会发生改变,每次都是从开头开始匹配,找到第一个匹配项就会停止匹配;返回的数组第一个元素是与正则表达式相匹配的文本,第二个元素是与正则表达中子表达式(分组)相匹配的文本(如果有的话),第三个元素和第二个类似,以此类推
  • g - 类似于test方法有g的时候,每次调用正则表达式的test方式之后,都会修改本身的lastIndex属性,也就是每次开始匹配的字符串的位置都不一样,那么就会导致匹配的结果不一致;每次匹配的返回值,如果匹配成功的话,就是数组,如果失败就是null,返回的数组第一个元素是与正则表达式相匹配的文本,第二个元素是与正则表达中子表达式(分组)相匹配的文本(如果有的话),第三个元素和第二个类似,以此类推

字符串中的RegExp

String.prototype.search(regexp)

用于检索字符串中制定的子字符串,或者检索与正则表达式相匹配的子字符串,方法返回第一个匹配结果的index(在string中的索引),查找不到返回-1;

如果传入的参数是字符串或者数字的话,JavaScript会隐式的将他们转换为正则表达式。

search方法不会执行全局匹配,将会忽略'g'标志,并且总是从字符串的开头开始匹配,

String.prototype.match(regexp)

检索字符串,找到一个或者多个与regexp匹配的文本

  • 没有g - 返回值和用正则表达式调用exac方法的返回值一样,只不过调用顺序变换了一下

在非全局检索的时候,match方法只能在字符串中进行一次匹配,如果没有任何匹配的文本,则返回null,否则返回一个数组,数组中存放着与他找到的匹配文本有关的信息,数组的第一个元素存放的是匹配的文本,其余的元素存放的是与子表达式匹配的文本,除了常规的数组元素之外,该数组包含两个特殊的属性,index表示声明匹配文本的其实字符在字符串的位置,input存放被检索的字符串string

  • 有g - match方法将执行全局检索,找到字符串中所有的匹配的子字符串,如果没有找到任何匹配项,返回null,如果找到一个或者多个,则返回一个数组,数组中存放的是所有的匹配子项,并且数组没有index和input属性

String.prototype.split(regexp)

使用正则表达式将字符串切割成数组

'a,b,c,d'.split(/,/); // [a,b,c,d]
'a1b2c3d4'.split(/\d/g); // [a,,b,c,d]
'a,b|f'.split(/,|\|/g) // [a,b,f]

String.prototype.replace(regexp)

replace函数的第一个参数可以是一个字符串,也可以是一个正则表达式,表示要找到的项,第二个参数是一个字符串或者是一个函数,表示要替换成的目标。

函数是在每次匹配替换的时候,就会调用一次,然后把函数执行的结果作为替换目标,函数有四个参数,第一个是匹配字符串,第二个是正则表达式分组内容(不一定会有,如果没有的话,后面的参数就会前移,如果有多个,后面的参数后移),第三个是匹配项在字符串中的index,第四个是原字符串

正则表达式常见的用法

判断数据类型

/**
* @param {Object} obj
* @return {String} 传入的对象的数据类型
*/
function getDataType(obj){
    let rst = Object.prototype.toString.call(obj);
    rst = rst.replace(/\[object\s(\w+)\]/,'$1'); // [object Xxx]
    return rst.toLowerCase()
}

提取浏览器url中的键值对

/**
* 参数为 window.location对象
* @param {Object} location
* @return {Object} url中search对象
*/
function getUrlParamObj(location){
    var obj = {};
    //获取url的参数部分
    var params = location.search.substr(1);
    //[^&=]+ 表示不含&或=的连续字符,加上()就是提取对应字符串
    params.replace(/([^&=]+)=([^&=]*)/gi, function(rs, $1, $2){
        obj[$1] = decodeURIComponent($2);
    });
    return obj;
}

实现HTML编码,将<>、/、"、、&等转译,避免xss攻击

function htmlEncode(str) {
    //匹配< / > " & `
    return str.replace(/[<>"&\/`]/g, function(rs) {
        switch (rs) {
            case "<":
                return "<";
            case ">":
                return ">";
            case "&":
                return "&";
            case "\"":
                return "'";
            case "/":
                return "/"
            case "`":
                return "'"
        }
    });
}

校验email

/**
* @param {String} email
* @return {Boolean} 是否是合法的email地址
*/
function validateEmail(email) {
    var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return re.test(String(email).toLowerCase());
}