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

相關文章
相關標籤/搜索