Javascript中的正则表达式

正则表达式提供了强大的字符串检索和操做的能力,这些能力在Javascript中有着比其余语言更普遍的应用。对于运行于浏览器环境中的Javascript,HTML文档的DOM操做和样式操做是其主要任务之一,正则表达式的非凡能力正能够应用于此,如:操做DOM节点的内容、解析元素选择器、根据属性值过滤和匹配元素等等。一般老是存在其它方式实现这些操做,但正则表达式可使咱们的实现更加简洁和优雅(一个例子)。javascript

一、Javascript中的正则表达式html

ECMAscript内置了标准的对象类型RegExp,提供两个原型方法(exec和test)用以进行正则匹配操做;字符串的包装类型String中,也有原型方法可使用正则表达式(match、replace、search和split)。下面着重讨论的是正则表达式在Javascript中的使用,正则表达式的基本规则请参见维基百科
java

二、RegExp对象建立正则表达式

第一种建立RegExp对象的方式是使用RegExp构造函数:数组

var rxp = new RegExp("\\s+javascript$","g");

其中第一个参数是描述正则表达式的字符串,第二个参数用来指定匹配模式。浏览器

另外一种更经常使用的方式是使用正则表达式字面量:函数

var rxp = /\s+javascript$/g;

这两种建立方式彻底等价。须要注意的是:使用构造函数方式建立RegExp对象时,第一个参数是一个字符串,所以参数中的“\”是须要转义的。字面量中的“\s”在字符串中要写成“\\s”。工具

正则表达式会在RegExp对象建立时编译,并且每次使用字面量都会建立一个新的RegExp对象(ECMAscript 5):oop

for (var i = 0; i < 10; i++) {
    /abc/.test("aaabbabccc"); //create an Object every time!
}

var r = /abc/; //create one Object and reuse it
for (var i = 0; i < 10; i++) {
    r.test("aaabbabccc");
}

一般咱们应该把正则表达式对象保存在一个变量中以便复用,防止生成过多的对象。性能

使用构造函数方式建立RegExp对象比使用字面量的一个便利之处,是可使用动态字符串建立正则表达式,好比检测某个DOM元素class中是否包含某个特定的classname:

function hasClass(className, id) {
    var elem = document.getElementById(id);
        reg = new RegExp("(^|\\s)" + className + "(\\s|$)");
    return reg.test(elem.className);
}

注:使用字面量方式也能够动态构建RegExp对象(eval("/(^|\\s)" + className + "(\\s|$)/")),但须要使用eval函数,于是不建议使用。

三、RegExp构造函数属性

Javascript为RegExp构造函数自己内置了一些属性,用于存储正则表达式执行过程当中的相关信息。每次执行了正则表达式操做,这些属性就会发生变化:

属性名 快写形式
说明
input $_ 最后一次匹配的字符串
lastMatch
$& 最后一次的匹配项
lastParen
$+ 最后一次的捕获项
leftContext $` input字符串中lastMatch以前的内容
rightContext $' input字符串中lastMatch以后的内容

此外$1 - $9被用来存储第一至九捕获项:

var html = "Foo<b class='hello'>Hello world!</b>Bar",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/;  
    // 注意pattern中的\1,指的是引用前面部分的第一个捕获项,若是你对正则中的捕获项不了解,请查阅之
var result = pattern.test(html);

console.log(result);               // true
console.log(RegExp.input);         // "Foo<b class='hello'>Hello world!</b>Bar"
console.log(RegExp.lastMatch);     // "<b class='hello'>Hello world!</b>"
console.log(RegExp["$`"]);         // "Foo"
console.log(RegExp["$'"]);         // "Bar"
console.log(RegExp.$1);            // "b"
console.log(RegExp.$2);            // " class='hello'"
console.log(RegExp.$3);            // "Hello world!"

咱们将在下面看到正则表达式的捕获功能在Javascript中的应用。

四、RegExp原型对象属性

Javascript中的正则表达式对象都会继承RegExp.prototype中的属性,并根据对象的建立表达式覆盖属性的值。其中的属性包括:source、global、ignoreCase、multiline和lastIndex。

其中source属性是正则表达式的字面量表示的字符串:

var reg1 = /abc(\w+)/,
    reg2 = new RegExp("abc(\\w+)");
console.log(reg1.source);           // "abc(\w+)"
console.log(reg2.source);           // "abc(\w+)"

能够看到,尽管reg2是调用构造函数生成,其source属性依然是字面量形式而非构建时传入的字符串。

global、ignoreCase和multiline属性均布尔值,分别标识正则表达式是否设置了g、i、m模式。(若是你对正则表达式的匹配模式不了解,请查阅之):

var reg1 = /abc(\w+)/gi,
    reg2 = new RegExp("abc(\\w+)", "m");
console.log(reg1.global);           // true
console.log(reg1.ignoreCase);       // true
console.log(reg1.multiline);        // false
console.log(reg2.ignoreCase);       // false

global、ignoreCase和multiline三个属性的Configurable、Enumerable和Writable均为false,也就是说这三个属性在正则对象建立以后便不可再改变。

上面的几个属性在实际应用中并无太大用处,最后一个属性lastIndex的值表示正则表达式下一次匹配开始的索引位置,咱们将在下面看到它的做用。

五、RegExp原型对象方法

RegExp对象定义了两个用于进行模式匹配的方法:exec和test。

exec函数接受一个字符串做为参数,用正则表达式对这个字符串进行模式匹配。exec的返回值是一个数组,数组的第一项是字符串中的第一个匹配项,若是表达式中有捕获项,则按照捕获顺序,把捕获项插入数组;若模式匹配失败,exec函数返回null:

var html = "<b class='hello'>Hello</b> <i>world!</i>",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/;

var match = pattern.exec(html);

console.log(match[0]);      // "<b class='hello'>Hello</b>"
console.log(match[1]);      // "b"
console.log(match[2]);      // " class='hello'"
console.log(match[3]);      // "Hello"

尽管html中有两个pattern的匹配项,但exec函数返回的数组只包含第一个匹配项的内容。返回值match是一个数组,但它被加入了额外的属性:input和index,input的值是进行模式匹配的字符串的内容,index的值是匹配项开始的索引位置:

var html = "Foo<b class='hello'>Hello</b> <i>world!</i>",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/;

var match = pattern.exec(html);

console.log(match.input);      // "Foo<b class='hello'>Hello</b> <i>world!</i>"
console.log(match.index);      // 3

上面所使用的例子中,正则表达式都没有加入匹配模式。当表达式的匹配模式被设置为全局(即加入g标志)时,exec函数的行为会有所不一样。全局匹配的涵义是表达式要匹配字符串中的每个匹配项,而不是普通模式中的只匹配第一个匹配项。看下面的示例,exec函数如何实现全局匹配的:

var html = "<b class='hello'>Hello</b> <i>world!</i>",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g;  // NOTE: add a "g"

var match = pattern.exec(html);
console.log(match[0]);      // "<b class='hello'>Hello</b>"
console.log(match[1]);      // "b"
console.log(match[2]);      // " class='hello'"
console.log(match[3]);      // "Hello"

match = pattern.exec(html);
console.log(match[0]);      // "<i>world!</i>"
console.log(match[1]);      // "i"
console.log(match[2]);      // ""
console.log(match[3]);      // "world!"

匹配模式设置为全局以后,咱们执行了两次exec函数,第一次执行返回的是第一个匹配项的内容,第二次执行返回的时第二个匹配项的内容。这是如何实现的呢?

还记得前面说过的RegExp对象的lastIndex属性吗?

当RegExp对象的匹配模式设置为全局时,执行exec函数:

一、从lastIndex标示的位置开始对字符串进行匹配,lastIndex值初始为0,即从字符串开始位置进行匹配;

二、若匹配成功,返回匹配项数组,并把lastIndex值设置为匹配项后面一个字符的索引;

三、若匹配失败,返回null,并把lastIndex值设置为0

所以上面的例子中,第二次执行exec函数时,匹配并非从字符串开始位置进行,而是从上一个匹配项的后面开始的:

var html = "<b class='hello'>Hello</b> <i>world!</i>",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g;  // NOTE: add a "g"

console.log(pattern.lastIndex);      // 0
var match = pattern.exec(html);      // start from 0
console.log(match);                  // Array
console.log(match.index);            // match at 0
console.log(pattern.lastIndex);      // 26
match = pattern.exec(html);          // start from 26
console.log(match);                  // Array
console.log(match.index);            // match at 27
console.log(pattern.lastIndex);      // 40
match = pattern.exec(html);          // start from 40
console.log(match);                  // null, match failed
console.log(pattern.lastIndex);      // 0

而当RegExp对象的匹配模式不是全局时,lastIndex的值一直为0,exec不管第几回执行都在字符串起始位置开始,只匹配字符串的第一项。

因而咱们能够利用全局模式匹配的行为遍历整个字符串并获取捕获内容:

var html = "<div class='test'><b>Hello</b> <i>world!</i></div>",
    tag = /<(\/?)(\w+)([^>]*?)>/g,
    match;
var num = 0;
while ((match = tag.exec(html)) !== null) {
    console.log(match);  // match every tag start and tag end
    num++;
}
console.log(num);        // 6

全局匹配模式有一个陷阱,就是当使用exec函数成功匹配一个字符串以后去匹配另外一个字符串,lastIndex属性并不会初始化为0,而是保留上一次执行exec后设置的值,这可能会引起错误:

var html1 = "<b class='hello'>Hello</b> <i>world!</i>",
    html2 = "<b class='foooo'>Foooo</b> <i>Bar</i>", // why here use "oooo" instead of "oo"?
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g;
    
var hello = pattern.exec(html1);
console.log(hello[3]);      // "Hello"
var foo = pattern.exec(html2);
console.log(foo[3]);        // Gotcha!!!  you want "Foooo" but got "Bar"

记得前面建立RegExp对象时,我建议你把它保存在一个变量里面以便复用吗?若是这是一个global模式的表达式,那么你要当心了,从新匹配一个新的字符串时,请确认lastIndex值在你的掌控之中!(使用直接量调用exec函数不会有这个问题,由于每次调用都是一个全新的RegExp对象。负负得正,是吧?^_^)

test行为与exec是等价的,区别是test函数不会返回匹配项的内容,而是返回一个说明匹配是否成功的布尔值。若是只须要验证字符串内容而不须要操做匹配项,那么使用test函数代替exec。

六、String对象原型方法

String对象中的方法,有四个可使用正则表达式:match、replace、search和split。

search方法是最简单的一个,它接受一个RegExp对象做为参数,返回字符串中第一个匹配的位置。若是传入的参数是一个字符串,则先调用RegExp构造函数将其转换为正则表达式。search方法会忽略全局匹配模式。

match方法的行为与RegExp对象的exec方法相似,它接受一个RegExp对象做为参数,返回一个包含匹配结果的数组,匹配失败时返回null。match方法也会受到表达式匹配模式的影响,全局模式和非全局模式的行为不一样:

一、全局模式时,match方法返回的数组元素是每一个匹配项;

二、非全局模式,match方法返回的数组元素是第一个匹配项,以及第一个匹配项中的捕获项:

var html = "<b>Hello</b> <i>world!</i>",
    pattern1 = /<(\w+)([^>]*)>(.*?)<\/\1>/g,
    pattern2 = /<(\w+)([^>]*)>(.*?)<\/\1>/;

console.log(html.match(pattern1)); // ["<b>Hello</b>", "<i>world!</i>"]
console.log(html.match(pattern2)); // ["<b>Hello</b>", "b", "", "Hello"]

与RegExp对象的exec方法不一样,String对象的match方法不会受到RegExp对象lastIndex属性的影响,match方法老是在字符串的起始位置开始匹配。但match方法以及其它三个方法,会把RegExp对象的lastIndex重置为0。

replace方法是四个方法中最复杂和有趣的一个。replace方法接受两个参数,第一个参数是一个RegExp对象,用于指定字符串中要替换的文本,第二个参数为替换文本的内容,能够是一个字符串或一个函数。replace方法的返回值为执行完替换以后的新字符串。

replace方法也受到匹配模式的影响:第一个参数的正则表达式模式为全局匹配时,替换字符串中全部匹配的位置;非全局模式时,只替换第一个匹配项。

replace方法的第二个参数是一个字符串时,可使用$符号引用匹配模式中的字符串,相似RegExp构造函数的属性:

$& 当前匹配项
$1 - $99 当前匹配项中的捕获项
$` 字符串中匹配项左边的内容
$' 字符串中匹配项右边的内容
$$ $符号
var html = "<b>Hello</b> <i>world!</i>",
    pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g;

html.replace(pattern,"$3");     // "Hello world!"

下面是ECMA-262文档中给出的例子,你能看明白吗?

"$1,$2".replace(/(\$(\d))/g, "$$1-$1$2");  // "$1-$11,$1-$22"

replace方法的第二个参数能够是一个函数,把函数的返回值做为替换内容。函数传入的参数分别是:

一、当前的匹配项

二、当前匹配项中的每个捕获项做为一个参数

三、当前匹配项在字符串中的索引位置

四、原始字符串

"$1,$2".replace(/(\$(\d))/g, function(){
    console.log(arguments.length);  // 5
    for (var k in arguments) {
        console.log(arguments[k]);
        // 1st loop:
        // arguments[0] : "$1" the match
        // arguments[1] : "$1" 1st capture
        // arguments[2] : "1" 2nd capture
        // arguments[3] : 0 match index
        // arguments[4] : "$1,$2" source string
        // 2nd loop:
        // arguments[0] : "$2" the match
        // arguments[1] : "$2" 1st capture
        // arguments[2] : "2" 2nd capture
        // arguments[3] : 3 match index
        // arguments[4] : "$1,$2" source string
    }
    return "$$1-$1$2";
});
// result: "$$1-$1$2,$$1-$1$2"

做为参数的函数中能够获取全部正则表达式匹配模式的捕获项,这给了咱们一种代替使用exec方式遍历操做字符串的方法。尽管replace方法的是用来进行字符串替换的,但咱们能够用它来作更多的事情。下面这个例子来自John Resig的《Secrets of the JavaScript Ninja》中,用来对url中的query字段进行归并操做:

function compress(source) {
    var keys = {};				
    source.replace(
        /([^=&]+)=([^&]*)/g,
        function(full, key, value) {			
            keys[key] =
            (keys[key] ? keys[key] + "," : "") + value;
            return "";
        });				
    var result = [];
    for (var key in keys) {
        result.push(key + "=" + keys[key]);
    }				
    return result.join("&");
}
var q = "foo=1&foo=2&blah=a&blah=b&foo=3"
console.log(compress(q));      //foo=1,2,3&blah=a,b

上面的例子中,使用replace方法遍历字符串,捕获数据并传入函数中,在函数中对捕获项进行操做。

一个用replace实现的字符串trim操做:

function trim(str) {
    return (str || "").replace(/^\s+|\s+$/g, "");				
} 
trim("    abcd    ");    // "abcd"

有人专门分析了各类replace方式实现trim的不一样性能表现,若是你有兴趣,能够参见这里

split方法用于分割字符串并返回一个数组。split方法能够接受一个RegExp对象做为边界来分割字符串,一般分割字符串不会被插入数组中,除非你设置了捕获项:

var html = "<b>Hello</b> <i>world!</i>";

html.split(/(<[^>]*>)/);  // ["", "<b>", "Hello", "</b>", " ", "<i>", "world!", "</i>", ""]

Javascript中正则表达式比字符串处理有更好的性能。


正则表达式的固有缺点是难以书写和维护,复杂的正则表达式可读性很是差,不少时候它都使人烦躁和心生厌倦。尽管如此,认真学习并把它变成一件好用的工具仍是值得的。


这篇文章写了整整一天,但愿能对花费时间阅读的人有所帮助,文章参考了如下资料,感谢做者们:

ECMA-262                                 W3C

Javascript高级程序设计(第3版)      Nicholas C.Zakas

Javascript权威指南(第6版)           David Flanagan

Secrets of the JavaScript Ninja    John Resig

JavaScript语言精粹                     Douglas Crockford

相关文章
相关标签/搜索