JavaScript:正則表達式的/y標識


本文要講的是一個ES6特性——正則表達式對象的/y標志的作用.該特性同時也是一個ES4特性,所以Firefox3+都原生支持,其他瀏覽器目前還沒有實現.

和/i對應於re.ingoreCase類似,/y標識對應的屬性是re.sticky(實際上這個y來自於yylex),sticky的意思是"粘滯".

和"全局匹配"類似,sticky屬性為true的正則表達式對象(有/y標識)的匹配過程稱之為"粘滯匹配"(反正我就這么叫了).

粘滯匹配會從兩個方面影響正則表達式的匹配方式.如下:

1.讀取和設置lastIndex屬性的值

以防你不知道lastIndex的作用,首先給一個最簡單的例子:

var re = /o/;
print(re.lastIndex); //0,lastIndex屬性的初始值為0
print(re.test(
"foo")); //true print(re.test("foo")); //true print(re.test("foo")); //true print(re.lastIndex); //0,lastIndex屬性的值沒有被更新
re.lastIndex = 10; //手動修改lastIndex屬性的值
print(re.test("foo")); //true,同樣可以匹配
print(re.lastIndex); //10

這應該是最常見的情況,如果一個正則對象不是全局(global)的,則其lastIndex屬性會被完全忽略(不會讀取,也不會賦值).但如果:

var re = /o/g;           //全局匹配
print(re.lastIndex); //0,lastIndex屬性的初始值為0 print(re.test("foo")); //true,匹配了第二個字符 print(re.lastIndex); //2,lastIndex屬性的值被設置為2,也就是第二個字符之后 print(re.test("foo")); //true,從第二個字符(lastIndex屬性的值)之后開始匹配,匹配了第三個字符 print(re.lastIndex); //3,lastIndex屬性的值被設置為2,也就是字符串的末尾 print(re.test("foo")); //false,已經沒有字符了,匹配失敗. print(re.lastIndex); //0,lastIndex屬性的值被重置為0 print(re.test("foo")); //true,又一次重新開始匹配 print(re.lastIndex); //2,一直循環下去 re.lastIndex = 3; //手動修改為3 print(re.test("foo")); //false,從第三個字符后開始匹配,所以匹配失敗

從上例中可以看出,全局匹配(/g)操作會讀取還會更新lastIndex屬性的值.類似於全局匹配,粘滯匹配(/y)也會讓這樣做.不過會有點區別,就是如果是全局匹配,在匹配失敗后,lastIndex屬性的值會被重置為0,但粘滯匹配不會.看下面的例子:

var re = /./y;           //粘滯匹配
print(re.test("foo")); //true print(re.lastIndex); //1 print(re.test("foo")); //true print(re.lastIndex); //2 print(re.test("foo")); //true print(re.lastIndex); //3 print(re.test("foo")); //false print(re.lastIndex); //3,匹配失敗后,lastIndex屬性沒有歸零 print(re.test("foo")); //false,所以匹配會一直失敗下去
re.lastIndex = 0; //同樣可以手動修改lastIndex屬性的值
print(re.test("foo")); //true

只有全局匹配和粘滯匹配才會讓引擎在匹配過程執行完畢后更新正則對象的lastIndex屬性的值,我們可以從SpiderMonkey源碼(jsregexp.cpp)中清晰的看到這一過程:

/* Update lastIndex. */
if (re->global() || (!rval->isNull() && re->sticky())) {    //如果是全局匹配或者(粘滯匹配且匹配成功時),才會進入下面的語句塊
    if (rval->isNull())                                     //如果匹配失敗,也就是全局匹配的匹配失敗
        obj->zeroRegExpLastIndex();                         //則把lastIndex屬性的值歸零
    else                                                    //否則,也就是全局匹配或者粘滯匹配匹配成功的情況
        obj->setRegExpLastIndex(lastIndexInt);              //則把lastIndexInt屬性的值設置為成功匹配的子字符串在原字符串中的偏移量
}

2."粘滯"的真正意義

看了第一節的介紹,你會發現我只提到了全局匹配和粘滯匹配的一個區別,就是在匹配失敗后要不要將lastIndex屬性的值重置為0.只有這些嗎?當然不是.這根本沒有體現出"粘滯"到底是什么意思.

我只用一個超簡單的例子就能演示出"粘滯"到底表現為什么:

/o/.test("foo")     //true
/o/y.test("foo")    //false

這個表現你能想通嗎?我用文字表述一下就是:粘滯匹配不能像普通正則那樣,跳過那些匹配失敗的字符后接着執行后面字符的匹配操作.在本例中就是,/o/在匹配字符f失敗之后,就完全失敗了,不會接着匹配f后面的o.為什么會這樣?我們還得和全局匹配比較一下:

var re = /^./g;
print(re.test("foo"));   //true
print(re.lastIndex);     //1
print(re.test("foo"));   //false
print(re.lastIndex);     //0

這個例子應該好懂,因為有^錨點(匹配字符串的開頭位置),所以第二次的匹配會失敗.然而:

var re = /^./y;
print(re.test("foo"));   //true
print(re.lastIndex);     //1
print(re.test("foo"));   //true
print(re.lastIndex);     //2

結論就是:全局匹配和粘滯匹配都會從字符串中由正則對象的lastIndex屬性的值指定的偏移位置處開始匹配,但區別是:粘滯匹配中,^元字符的意義變了,它代表的不是整個字符串的開頭位置,而代表的就是這個偏移位置.所以上面的兩次匹配都能成功.同時,每個粘滯正則中不管第一個字符是不是^元字符,都會被隱式的加上^.回到剛才那個讓然費解的例子就是:

/o/y.test("foo") 

就相當於

/^o/y.test("foo") 

這下能看懂了吧.^f才能匹配,^o不行.

為了再次證明一下粘滯匹配中的^的位置會動態改變,看下面的例子:

var re = /o/y;           //相當於/^o/y
print(re.test("foo"));   //false
print(re.lastIndex);     //0
re.lastIndex = 1;        //手動跳過了第一個字符f,^現在匹配的位置就是f和o之間的位置,所以^o能匹配.
print(re.test("foo"));   //true
print(re.lastIndex);     //2,^現在匹配的位置就是o和o之間的位置,所以^o能匹配.
print(re.test("foo"));   //true
print(re.lastIndex);     //3
print(re.test("foo"));   //false
print(re.lastIndex);     //3

現在懂了吧,粘滯的意思就是"使用隱式的^錨點把正則錨定在了lastIndex所指定的偏移位置處".

總結

網上幾乎沒有講正則的/y標識的帖子或文章,Brendan Eich說的一段話給了我很大幫助來理解這個東西.

/y標識讓一個未錨定的正則只在目標字符串的當前位置匹配成功或匹配失敗./g或其他東西也會影響當前位置具體在哪里.但比起其他因素,/y是完全獨立的(更底層).

本文中我只使用了正則的test方法來講解/y標識,你應該再用其他正則相關的方法試驗一下:RegExp.prototype.exec,String.prototype.search,String.prototype.split,String.prototype.match,String.prototype.replace.以及gy標識同時存在的情況等.

另外,其他語言比如Perl(正則表達式最強大最靈活的語言)中,怎么沒有這個標識呢?答案是:人家有\G元字符,\G是個零寬斷言,表示的是上次成功匹配時的偏移位置.

my $str = "foo";
pos($str) = 0;               #相當於設置lastIndex為0
while ($str =~ /(\Go)/g) {   #匹配失敗,不會進入這個循環
    print $1;
}
pos($str) = 1;               #相當於設置lastIndex為1,跳過字符f
while($str =~ /(\Go)/g) {    #匹配成功,進入循環
    print $1;                #輸出兩次o
    print pos($str)          #第一次輸出2,第二次輸出3
}

還有,比起其他語言中的正則,JavaScript中的正則其實是"弱爆了",如果這都學不會,呵呵...

對我來說,沒有正則幾乎相當於沒有Google.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM