想要在JS中把正則玩得飄逸,學會這幾個函式的使用必不可少

dreamapplehappy發表於2020-08-24

image

在之前的一系列文章中,我們講解了很多關於正規表示式的知識。那麼作為一個前端工程師,如果想要把這些知識應用到我們平時的開發中去的話,就需要知道在JavaScript中,能夠使用正則的函式有哪些?然後它們各自的功能是什麼?有哪些需要注意的地方?只有掌握好了每一個方法的使用場景,我們才可能在需要使用的時候能夠很快的想起來使用哪個方法效率最高,效果最好。

這些確實是一些基礎的知識,但是我相信應該有很多同學還沒有系統的把這些知識學習一邊。相信我,如果你能夠把這篇文章看完的話,你肯定可以學習到一些新的知識。知道每一個方法的用途,使用場景,學會在合適的場景選擇合適的方法。當然你還能夠掌握這些方法需要注意的地方,以防在以後使用的時候陷入了困境。

文章中的程式碼示例如果沒有特別說明的話,都是在Chrome瀏覽器中進行的。本篇文章的內容比較長,建議先收藏起來,可以以後慢慢細看。

JavaScript中,能夠使用正規表示式的函式有(排除了過時的方法):

RegExp.prototype

首先我們要講解的是RegExp物件上的兩個方法

RegExp.prototype.test()

  • 作用:檢測給定的字串中是否有滿足正則的匹配
  • 程式碼示例

簡單的匹配,根據匹配的結果確定是否匹配成功。

const reg = /\d{4}-\d{2}-\d{2}/;
const str1 = '2000-02-22';
const str2 = '20-20-20';
console.log(reg.test(str1)); // true
console.log(reg.test(str2)); // false

上面的正規表示式沒有設定全域性的標誌符g,如果設定了全域性的標誌符的話,我們在使用這個方法的時候就要小心一些了。因為如果正規表示式設定了全域性的識別符號g,那麼對於同一個正規表示式來說,在執行test方法的時候,如果匹配成功的話,它會修改這個正則物件的lastIndex屬性,可能會在下次匹配的時候導致一些問題,我們下面來看一個例子

const reg = /abc/g;
const str1 = 'abcd';
const str2 = 'abcdabcd';

console.log(reg.lastIndex);  // 0
console.log(reg.test(str1));  // true
console.log(reg.lastIndex);  // 3
console.log(reg.test(str1));  // false

console.log(reg.lastIndex);  // 0
console.log(reg.test(str2));  // true
console.log(reg.lastIndex);  // 3
console.log(reg.test(str2));  // true

上面的例子很好地說明了這種情況,如果我們設定了全域性識別符號g的話,只要我們當前的匹配是成功的,那麼接下來如果再次使用同樣的正則進行匹配的話就可能會出現問題,因為上一個成功的匹配導致正規表示式物件的lastIndex屬性的值發生了變化,那麼下次進行匹配的時候是從lastIndex位置開始的,所以就可能會出現一些問題

  • 注意事項:如果在使用test方法的時候,需要注意正規表示式是否帶有g識別符號。如果這個正規表示式需要進行多次的匹配的話,最好不要設定g識別符號。除非你知道自己確實需要這樣做。
  • 使用場景

假如有這樣一個需求,你需要判斷使用者輸入的使用者名稱是否滿足需求,需求如下:(1)使用者名稱長度需要是8-16位。(2)使用者名稱可以包含數字,字母(大小寫都可以),下劃線。(3)數字和字母是必須包含的

當然對於熟悉正規表示式的你來說,這不是一個問題,能用一行程式碼解決的問題絕不用兩行程式碼去解決。你可以很快可以通過使用test方法來解決這個問題。

const validNameRE = /^(?=_*(?:\d+_*[a-zA-Z]+|[a-zA-Z]+_*\d+))\w{8,16}$/;
// 假如這是使用者輸入的使用者名稱
const userInputName = '1234567890';
// 檢查使用者輸入的使用者名稱是否合乎要求
const isValidName = validNameRE.test(userInputName); // false

在平時的開發中,如果需要判斷頁面所處的宿主環境的話,我們也會使用test方法去判斷當前頁面所處的環境。例如,你需要判斷當前頁面所處的環境是不是iPhone的話,你可能會寫出這樣的判斷:

const iPhoneReg = /iPhone/;
console.log(iPhoneReg.test(navigator.userAgent));  // true

RegExp.prototype.exec()

  • 作用:這個方法是比較常用的一個方法,在給定的字串中進行匹配,返回一個匹配的結果陣列或者null。通常情況下我們會使用這個方法來提取字串中符合匹配的一些字串。
  • 程式碼示例

需要注意的是,如果沒有符合的匹配,返回的結果是null,而不是一個空陣列[]。所以當我們需要判斷是否有匹配的結果的時候,不能憑感覺覺得返回的值是一個空的陣列[]

const reg1 = /(\d{2}):(\d{2}):(\d{2})/;
const str1 = 'Sat Aug 22 2020 17:31:55 GMT+0800 (中國標準時間)';
const str2 = 'Sat Aug 22 2020';

console.log(reg1.exec(str1));  // ["17:31:55", "17", "31", "55", index: 16, input: "Sat Aug 22 2020 17:31:55 GMT+0800 (中國標準時間)", groups: undefined]
console.log(reg1.exec(str2));  // null

從上面的程式碼中我們可以看到,如果沒有匹配結果的話,返回的結果是null。如果能夠匹配成功的話,返回的結果是一個陣列。在這個結果陣列中,第0項表示正規表示式匹配的內容。其中第1..n項表示的是正規表示式中括號的捕獲內容,對於上面的示例來說,第1..3項表示的是捕獲時間的時分秒。陣列還有額外的屬性indexinput,其中index表示正規表示式匹配到的字串在原字串中的位置。input表示原始待匹配的字串。

  • 注意事項

    • 注意正規表示式是否設定了g識別符號,如果設定了g識別符號,那麼我們可以使用這個正規表示式進行全域性的搜尋。可以看下面的程式碼示例。
    const reg = /\d/g;
    const str = '654321';
    let result;
    while ((result = reg.exec(str))) {
      console.log(
        `本次匹配到的數字是:${result[0]}, 正規表示式的 lastIndex 的值是:${
          reg.lastIndex
        }`
      );
    }

輸出的結果如下:

本次匹配到的數字是:6, 正規表示式的 lastIndex 的值是:1
本次匹配到的數字是:5, 正規表示式的 lastIndex 的值是:2
本次匹配到的數字是:4, 正規表示式的 lastIndex 的值是:3
本次匹配到的數字是:3, 正規表示式的 lastIndex 的值是:4
本次匹配到的數字是:2, 正規表示式的 lastIndex 的值是:5
本次匹配到的數字是:1, 正規表示式的 lastIndex 的值是:6

需要注意的是,如果上面匹配的正規表示式沒有設定g識別符號,或者在while迴圈的條件判斷中使用的是正規表示式的字面量的話,都會造成“死迴圈”。因為那樣的話,每次迴圈開始的時候,正規表示式的lastIndex屬性都會是0,導致result一直都是有值的,所以就導致了“死迴圈”。所以我們在while迴圈中使用exec方法的時候一定要小心一些

  • 使用場景:這個方法主要用來在原始文字中提取一些我們想要的關鍵資訊,所以只要是這樣的一個需求場景,都可以使用正規表示式的exec方法去處理。比如:

    • 對使用者輸入內容中的連結進行自動識別,然後對相應的連結內容進行樣式和功能上的處理。
    • 可以提取url中的查詢引數,如果我們需要自己把url中的查詢引數提取出來的話,使用exec方法也是一個選擇。
    • 如果你閱讀過vue的原始碼的話,在編譯模組中的文字解析使用到了exec方法,有興趣的話大家可以看一看相關的程式碼實現。

當然還有很多的場景可以使用exec方法去處理的,大家在平時的開發中有沒有使用過exec方法處理一些問題呢?可以在下面留言,我們大家一起討論一下,加深一下對這個方法的理解。

String.prototype

接下來我們來講解一下String.prototype上面有關正則的一些方法。

String.prototype.match()

  • 作用:這個方法返回字串匹配正規表示式的結果。
  • 程式碼示例
const reg = /\d/;
const str = 'abc123';
console.log(str.match(reg));  // ["1", index: 3, input: "abc123", groups: undefined]
  • 注意事項

    • 沒有匹配到結果的返回結果是null
    const reg = /\d/;
    const str = 'abc';
    console.log(str.match(reg));  // null
    • 是否設定了g識別符號,如果沒有設定g的話,match的返回結果跟對應的exec的返回結果是一樣的。如果設定了g識別符號的話,返回的結果是與正規表示式相匹配的結果的集合。
    const reg = /\d/g;
    const str = 'abc123';
    console.log(str.match(reg));  // ["1", "2", "3"]
    • 如果match方法沒有傳遞引數的話,返回的結果是[''],一個包含空字串的陣列。
    const str = 'abc123';
    console.log(str.match());  // ["", index: 0, input: "abc123", groups: undefined]
    • 如果match方法傳遞的引數是一個字串或者數字的話,會在內部隱式呼叫new RegExp(regex),將傳入的引數轉變為一個正規表示式。
    const str = 'abc123';
    console.log(str.match('b'));  // ["b", index: 1, input: "abc123", groups: undefined]
  • 使用場景

    簡單獲取url中的查詢引數:

    const query = {};
    // 首先使用帶有g識別符號的正則,表示全域性查詢
    const kv = location.search.match(/\w*=\w*/g);
    if (kv) {
      kv.forEach(v => {
          // 使用不帶g識別符號的正則,需要獲取括號中的捕獲內容
        const q = v.match(/(\w*)=(\w*)/);
        query[q[1]] = q[2];
      });
    }

String.prototype.matchAll()

  • 作用:這個方法返回一個包含所有匹配正規表示式以及正規表示式中括號的捕獲內容的迭代器。需要注意的是這個方法存在相容性,具體內容可以檢視String.prototype.matchAll
  • 程式碼示例
const reg = /(\w*)=(\w*)/g;
const str = 'a=1,b=2,c=3';
console.log([...str.matchAll(reg)]);

String.prototype.matchAll()

  • 注意事項

    • match方法相同的地方是,如果傳遞給matchAll方法的引數不是一個正規表示式的話,那麼會隱式呼叫new RegExp(obj)將其轉換為一個正規表示式物件。
    • 傳遞給matchAll的正規表示式需要是設定了g識別符號的,如果沒有設定g識別符號,那麼就會丟擲一個錯誤。
    const reg = /(\w*)=(\w*)/;
    const str = 'a=1,b=2,c=3';
    console.log([...str.matchAll(reg)]);  // Uncaught TypeError: String.prototype.matchAll called with a non-global RegExp argument
    • 在可以使用matchAll的情況下,使用matchAll比使用exec方法更便捷一些。因為在全域性需要匹配的情況下,使用exec方法需要配合迴圈來使用,但是使用matchAll就可以不使用迴圈。
    • matchAll方法在字串執行匹配的過程中,正規表示式的lastIndex屬性不會更新。更多詳情可以參考String.prototype.matchAll()
  • 使用場景

還是以上面的獲取url中的查詢引數這個小功能來實踐一下:

const query = {};
const kvs = location.search.matchAll(/(\w*)=(\w*)/g);
if (kvs) {
    for (let kv of kvs) {
        query[kv[1]] = kv[2];
    }
}
console.log(query);

String.prototype.replace()

  • 作用:這個方法在平時的開發中應該比較常用,那麼它的作用就是使用替換物replacement替換原字串中符合某種模式pattern的字串。其中替換物可以是一個字串,或者返回值是字串的函式;模式可以是正規表示式或者字串。
  • 程式碼示例

    因為這個函式的入參可以是不同的型別,所以對每種型別的入參我們都來實踐一下吧。

    • pattern是字串,replacement也是字串。這種形式在平時的開發中使用的比較多。
    const pattern = 'a';
    const replacement = 'A';
    const str = 'aBCD';
    console.log(str.replace(pattern, replacement));  // ABCD
    • pattern是正規表示式,replacement是字串
    const pattern = /__(\d)__/;
    const replacement = "--$$--$&--$`--$'--$1--";
    const str = 'aaa__1__bbb';
    console.log(str.replace(pattern, replacement));  // aaa--$--__1__--aaa--bbb--1--bbb

如果replacement是字串,那麼在這個字串中可以使用一些特殊的變數,具體可參考Specifying a string as a parameter

  • pattern是正規表示式,replacement是函式

    const pattern = /__(?<number>\d)__/;
    const replacement = function(match, p1, offset, str, groups) {
      console.log(`匹配到的字串是:${match}\n捕獲到的內容是:${p1}\n匹配的位置是:${offset}\n原始待匹配的字串是:${str}\n命名的捕獲內容是:${JSON.stringify(groups)}`);
      return '======';
    };
    const str = 'aaa__1__bbb';
    console.log(str.replace(pattern, replacement)); // aaa======bbb

其中控制檯的輸出如下所示:

匹配到的字串是:__1__
捕獲到的內容是:1
匹配的位置是:3
原始待匹配的字串是:aaa__1__bbb
命名的捕獲內容是:{"number":"1"}

如果你對replacement是函式這種情況不是很瞭解的話可以看看Specifying a function as a parameter,裡面會有詳細的解釋,這裡就不在具體解釋了。

  • 注意事項

    需要注意的地方就是當我們的pattern是正規表示式的時候,要注意是否設定了g識別符號,因為如果沒有設定g識別符號的話,只會進行一次匹配。設定了g識別符號的話,會進行全域性的匹配。

  • 使用場景

    對於前端來說,對使用者的輸入進行校驗時很常見的需求。假如我們有一個輸入框,只允許使用者輸入數字,我們可以這樣處理:

    const reg = /\D/g;
    const str = 'abc123';
    console.log(str.replace(reg, ''));  // 123

    這樣就能夠保證使用者的輸入只有數字了。

String.prototype.replaceAll()

As of August 2020 the replaceAll() method is supported by Firefox but not by Chrome. It will become available in Chrome 85.

這個方法和replace方法的作用差不多,從名字上就能夠知道replaceAll是全域性的替換。因為這個方法的相容性問題,我們需要在Firefox瀏覽器上進行試驗。

const pattern = 'a';
const replacement = 'A';
const str = 'aBCDa';
console.log(str.replace(pattern, replacement));  // ABCDa
console.log(str.replaceAll(pattern, replacement));  // ABCDA
  • 注意事項:如果給函式傳遞的pattern引數是個正規表示式的話,這個正規表示式必須設定了g識別符號,不然會丟擲一個錯誤。
const pattern = /a/;
const replacement = 'A';
const str = 'aBCDa';
console.log(str.replace(pattern, replacement));  // ABCDa
console.log(str.replaceAll(pattern, replacement));  // Uncaught TypeError: replaceAll must be called with a global RegExp

String.prototype.search()

  • 作用:這個方法用來在字串中尋找是否含有特定模式的匹配,如果找到對應的模式,返回匹配開始的下標;沒有找到的話返回-1
  • 程式碼示例
const reg = /\d/;
const str1 = '123';
const str2 = 'abc';
console.log(str1.search(reg));  // 0
console.log(str2.search(reg));  // -1
  • 注意事項

    • 如果傳入的引數不是一個正規表示式的話,會隱式的呼叫new RegExp(regexp)將其轉換為一個正規表示式。
    • 沒有找到相應匹配的時候,返回的值是-1;所以大家在使用這個方法做判斷的時候要注意,只有返回值是-1的時候,才表示沒有找到相應的匹配。
  • 使用場景

如果你需要找到特定匹配在字串中的位置的話,那麼可以使用search方法。

const reg = /\d/;
const str = 'abc6def';
console.log(str.search(reg));  // 3

String.prototype.split()

  • 作用:將一個字串按照分割器進行分割,將分割後的字串片段組成一個新的陣列,其中分割器separator可以是一個字串或者一個正規表示式。
  • 程式碼示例

    • 分割器separator是字串:
    const str = 'hello, world!';
    console.log(str.split(''));  // ["h", "e", "l", "l", "o", ",", " ", "w", "o", "r", "l", "d", "!"]
    • 分割器separator是正規表示式:
    const str = 'abc1abc2abc3';
    const separator = /\w(?=\d)/;
    console.log(str.split(separator));  // ["ab", "1ab", "2ab", "3"]
  • 注意事項

    • 如果split方法沒有傳遞引數的話,會返回一個包含原字串的陣列:
    const str = 'hello, world!';
    console.log(str.split());  // ["hello, world!"]
    • 因為JavaScript的字串是使用UTF-16進行編碼的,該編碼使用一個16位元的編碼單元來表示大部分常見的字元,使用兩個編碼單元表示不常用的字元。所以對於一些不常用的字元來說,在使用split方法進行字串分割的時候可能會出現一些問題:
    const str = '??????';
    console.log(str.split(''));  // ["�", "�", "�", "�", "�", "�", "�", "�", "�", "�", "�", "�"]

如何解決這種型別的問題呢?第一種方法是使用陣列的擴充套件運算子:

const str = '??????';
console.log([...str]);  // ["?", "?", "?", "?", "?", "?"]

第二種方法是使用設定了u識別符號的正規表示式:

const str = '??????';
const separator = /(?=[\s\S])/u;
console.log(str.split(separator)); // ["?", "?", "?", "?", "?", "?"]
    • 如果傳入的正則表達引數中含有捕獲的括號,那麼捕獲的內容也會包含在返回的陣列中:
    const str = 'abc1abc2abc3';
    const separator = /(\w)(?=\d)/;
    console.log(str.split(separator));  // ["ab", "c", "1ab", "c", "2ab", "c", "3"]
    • split方法還可以傳入第二個引數,用來控制返回的陣列的長度:
    const str = 'hello, world!';
    console.log(str.split('', 3));  // ["h", "e", "l"]
    • 使用場景

    在實際的開發中,最常用的場景就是將一個字串轉換為一個陣列了:

    const str = 'a/b/c/d/e';
    console.log(str.split('/')); // ["a", "b", "c", "d", "e"]

    總結

    當我們能夠把上面的這些方法都熟練的掌握之後,那麼在實際的開發中再結合正規表示式來使用的話,那簡直就是如虎添翼,能夠在一些場景下提高我們開發的效率。

    當然光靠看看文章是不能夠很好地將這些知識點都記牢固的,你需要的是一個一個的實踐一下,這樣才能夠加深自己的記憶,才能夠記得更牢固

    如果大家還想了解更多關於正規表示式的知識點的話,可以看看我之前寫的一系列的文章:

    如果你對本篇文章有什麼意見和建議,都可以直接在文章下面留言,也可以在這裡提出來。也歡迎大家關注我的公眾號關山不難越,學習更多實用的前端知識,讓我們一起努力進步吧。

    相關文章