# 第十章 正規表達式 ###### tags: `好想工作室`、`忍者讀書會` ## ◼ 測試正規表達式 * [Regular Expression Test Page for JavaScript](https://www.regexplanet.com/advanced/javascript/index.html) * [regex101](https://regex101.com/) regex101對於初學者特別有用,因為他會自動對正規表達式的執行結果做說明。 ![](https://i.imgur.com/lv0qAT6.jpg) ## ◼ 10.1 為什麼正規表達式這麼重要 以美國郵遞區號作為例子,美國郵遞區號的格式是5+4碼,中間以一道橫線分開(e.g. 99999-9999) 現在我們要寫出一個函式,來檢查輸入的字串是否符合美國郵局的標準。 #### ▌程式列表10.1 檢查字串是否符合某種特定樣式 ```javascript= function isThisAZipCode(candidate) { if (typeof candidate !== "string" || candidate.length != 10) { return false; } for (let n = 0; n < candidate.length; n++) { let c = candidate[n]; switch (n) { case 0: case 1: case 2: case 3: case 4: case 6: case 7: case 8: case 9: if (c < '0' || c > '9') return false; break; case 5: if (c != '-') return false; break; } } return true; } ``` 但如果改為利用正規表達式,則只需要兩行即可解決: ```javascript! function isThisAZipCode(candidate){ return /^\d{5}-\d{4}$/.test(candidate); } ``` ## ◼ 10.2 複習正規表達式 * **推薦書籍:** * [Mastering Regular Expressions](https://www.amazon.com/Mastering-Regular-Expressions-Jeffrey-Friedl/dp/0596528124)(Jeffrey E. F. Friedl) * [Introducing Regular Expressions](https://www.oreilly.com/library/view/introducing-regular-expressions/9781449338879/)(Michael Fitzgerald) * [Regular Expressions Cookbook](https://www.amazon.com/Regular-Expressions-Cookbook-Solutions-Programming/dp/1449319432)(Jan Goyvaerts, Steven Levithan ) ### ▉ 10.2.1 何謂正規表達式 * **何謂正規表達式?** 1. 「正規表達式」源自數學家Stephen kleene,他使用所謂的正規集來描述自動計算機的模型。 ![](https://i.imgur.com/5lLYjtW.png) 2. 用簡單的方式來描述,正規表達式是進行文字匹配的一種方式,並由詞彙與運算子所組成。 * **在JS中建立正規表達式** 在JavaScript裡,有兩種方式來建立正規表達式。 1. Regular expression literals 正規表達式實值(或稱正規表達式字面值),就像字串是用一對引號(`""`)括住一樣,正規表達式則是使用一對正斜線(`//`)。 ```javascript! const pattern = /test/; ``` 2. Function Constructor 建構器函式,呼叫 RegExp 物件的建構函式 ```javascript! const pattern = new RegExp("tset"); ``` **** * ++Regular expression literals++ VS. ++Function Constructor++ | 建立方式 | Regular expression literals | Function Constructor | | -------- | --------------------------- | ------------------------ | | 編譯 | script載入即編譯 | 執行期間才編譯 | | 效能 | 不易更改、彈性小 -> 效能佳 | 易更改、彈性大 -> 效能差 | | 適合 |適合用於正規表達式內容不變動時 |適合用於正規表達式可能會動態改變| * 大多數傾向使用實值(Regular expression literals),原因在於反斜線(`\`)在正規表達式中是一個重要字元,同時也是字元實值裡的跳脫字元,所以在字串裡呈現反斜線時要寫成「`\\`」,因此當我們是使用字串來撰寫正規表達式時,會讓原本就不容易理解的語法看起來更加怪異。 e.g. 要處理的字串有以下三個: (紅字為特殊字元,要記得使用跳脫字元(`\`)處理成一般字元) (i.e. 跳脫字元可以去除特殊字元的功用,讓特殊字元回歸成普通的字元) 1. <font color= "red">`.`</font>`article-content-inner`<font color= "red">`.`</font>`zoom1 `<font color= "red">`*`</font> 2. <font color= "red">`.`</font>`article-content-inner`<font color= "red">`.`</font>`zoom2 `<font color= "red">`*`</font> 3. <font color= "red">`.`</font>`article-content-inner`<font color= "red">`.`</font>`zoom3 `<font color= "red">`*`</font> 先講以上3個字串合併成一條規則: <font color= "red">`.`</font>`article-content-inner`<font color= "red">`.`</font>`zoom[1-3] `<font color= "red">`*`</font> 加上跳脫字元(`\`),前後加上正斜線(`/`),完成實值表達式: <font color= "blue">`/`</font><font color= "red">`\.`</font>`article-content-inner`<font color= "red">`\.`</font>`zoom[1-3] `<font color= "red">`\*`</font><font color= "blue">`/`</font> 若要轉成字串: <font color= "blue">`"`</font><font color= "red">`\\.`</font>`article-content-inner`<font color= "red">`\\.`</font>`zoom[1-3] `<font color= "red">`\\*`</font><font color= "blue">`"`</font> 兩種寫法的比較: ```javascript! var re = /\.article-content-inner\.zoom[1-3] \*/; var re = new RegExp("\\.article-content-inner\\.zoom[1-3] \\*"); ``` **** * **旗標(flag)** 除了正規表達式的主體外,還可以有以下5個旗標(flag)。 1. i (ignore case):不區分大小寫 2. g (global search):匹配所有符合樣式的地方 (預設:區域匹配,只會匹配到第一個符合樣式的地方) 3. m (multiline):允許對多行文字做匹配,例如對網頁上的多行文字區取值 (參考資料:[Multiline mode of anchors ^ $, flag "m"](https://javascript.info/regexp-multiline-mode)) 4. y (sticky):進行黏性匹配(sticky matching)。一個字串做黏性匹配的正規表達式,只會在最後一次符合處之後進行匹配。 (參考資料:[What does regex' flag 'y' do?](https://stackoverflow.com/questions/4542304/what-does-regex-flag-y-do)) e.g. ```javascript! var str = "a0bc1"; var rexWith = /\d/y; //匹配十進位數字 console.log(rexWith.exec(str)); //null rexWith.lastIndex = 0; console.log(rexWith.exec(str)); //null rexWith.lastIndex = 1; console.log(rexWith.exec(str)); //['0', index: 1, input: 'a0bc1', groups: undefined] rexWith.lastIndex = 2; console.log(rexWith.exec(str)); //null rexWith.lastIndex = 3; console.log(rexWith.exec(str)); //null rexWith.lastIndex = 4; console.log(rexWith.exec(str)); //['1', index: 4, input: 'a0bc1', groups: undefined] ``` 5. u (unicode):對Unicode數值碼做跳脫。 (參考資料:[Unicode: flag "u" and class \p{...}](https://javascript.info/regexp-unicode)) 大多數字元為2bytes,但有些特殊字元(像是數學符號𝑥、表情符號😄或一些象形文字等等)就佔了4bytes,這時候如果想知道一個字串的長度,可能會有不正確的結果。 e.g. ```javascript! console.log('a'.length); //1 console.log('😄'.length); //2 ``` 會有以上結果,主要是因為length會將1個「4bytes的字元」當作2個「2bytes的字元」去處理(這裡會牽扯到「代理對(surrogate pair)」觀念)。 而在正規表達式中,也會出現將1個「4bytes的字元」當作2個「2bytes的字元」這樣的現象,因此正規表達式有flag u來解決這個問題,更重要的是加上flag u後,我們可以直接用unicode去做匹配,以及也可以利用字元的屬性(\p{…},例如不同語言、貨幣符號等)做匹配。 e.g. 用unicode匹配: ```javascript! let letter_a = "a"; let regex = /\u{0061}/ug; console.log(regex.exec(letter_a)); //['a', index: 0, input: 'a', groups: undefined] ``` e.g. 中文字匹配: ```javascript! let str = `Hello Привет 你好 123_456`; let regexp = /\p{sc=Han}/gu; // returns Chinese hieroglyphs alert( str.match(regexp) ); // 你,好 ``` ### ▉ 10.2.2 詞彙和運算子 * **完全符合** e.g. /test/ 表示必須完全符合「test」才能匹配到。 ```javascript! let str = "tes te t test tset tse te t"; let regex = /test/; str.match(regex); //['test', index: 9, input: 'tes te t test tset tse te t', groups: undefined] ``` **** * **匹配字元集合** 利用集合運算子(set operator,用中括號`[]`表示) 1. **[abc]** 會尋找abc中的任意字元,只有符合a或b或c即可匹配到。 ```javascript! let str = "adfghjkbfghc"; let regex = /[abc]/g; //後方使用g flag的用意是想進行全域的搜尋與捕捉 //以下是分別用三次,而非一次完成 regex.exec(str); // ['a', index: 0, input: 'adfghjkbfghc', groups: undefined] regex.exec(str); // ['b', index: 7, input: 'adfghjkbfghc', groups: undefined] regex.exec(str); // ['c', index: 11, input: 'adfghjkbfghc', groups: undefined] ``` 圖示: ![](https://i.imgur.com/sJGcaXH.png) [註] exec() 如果匹配失敗,會回傳null,並且將該正規表達式的`lastIndex`重置為0。 如果匹配成功,會回傳一組數組(如上面範例),並更新該正規表達式的`lastIndex`屬性。 [註] lastIndex 回傳一個數字,該數字用來指定下一次匹配開始搜索的地方。 e.g. ```javascript! let str = 'table football, foosball'; let regex = /foo/g; regex.exec(str); console.log(regex.lastIndex); //9 ``` 圖示: ![](https://i.imgur.com/BDOxLzd.jpg) 2. **[^abc]** 在左側括號後面加上「^」,會尋找abc「之外」的任意字元。 e.g. ```javascript! let str = "abdc"; let regex = /[^abc]/g; regex.exec(str); //d ``` 3. **[a-m]** 指定一個範圍,此處表a到m(包含m)之間的字元都在這個集合之中。 原本我們依第一點方式得寫成[abcdefghijklm],現在只需利用[a-m]表達即可。 **** * **跳脫** 不是所有字元的含意都跟字面上看起來那樣,例如說前面已經提到的一些特殊字元,例如「`[`」、「`]`」、「`^`」、「`-`」,還有像是「`$`」、「`.`」也都是特殊字元,他們在正規表達式中會有其他的功用,而非單純的顯示字元樣式。 因此若我們只想要他們的字元樣式,在正規表達式中可以利用反斜線(`\`)來跳脫其後的任何字元,使他們符合字面意義的匹配詞彙。 e.g. 「`\[`」表示「`[`」這個字元的樣式,而不是字元集合的左側中括號。 **** * **開始與結束** 1. 開始:在正規表達式中的起始處使用插入字元(`^`),將會匹配固定在字串開頭的部分。 (這是插入字元(`^`)的重載,因為它也可以放在集合,作為否定之意) e.g. /^test/ 只會符合以test開頭的字串。 ![](https://i.imgur.com/5V9ubfU.png) 2. 結束:在正規表達式中的最尾端使用錢字號(`$`),他代表一個樣式必須出現在字串的最尾端。 e.g. /test$/ 只會符合以test結尾的字串。 ![](https://i.imgur.com/9u9kWge.png) 3. 開始與結束:同時使用`^`與`$`,代表這個樣式必須佔據要匹配的整個字串。 e.g. /^test$/ 只會符合為test的字串。 ![](https://i.imgur.com/lKkmeKd.png) **** * **重複符合** | 字元 | 描述 | 範例 | | ----- | ---------------------------------------------------------------- | ---------------------------------------------------- | | ? | 代表前面的字元可以出現一次,或完全不出現(0次或1次) | /t?est/符合test和est | | + | 代表前面的字元可以出現至少出現一次(1次或多次) | /t+est/會符合test、ttest、tttest </br> 但不會符合est | | * | 代表前面的字元可以不出現,也可以出現一次或多次(0次、1次或多次) | /t*est/符合test、ttest、tttest以及est | | {n,m} | 指定重複出現次數的範圍 | /a{4,10}/代表a字元要連續出現四到十次 | | {n} | 指定重複出現的次數 | /a{4}/代表a字元要連續出現四次 | | {n, } | 省略第二個數字,則會變開放式範圍 | /a{4,}/代表a字元可連續出現四次以上 | [補充] **貪婪(greedy)、不貪婪的(nongreedy)** 以上重複運算子可以是貪婪(greedy)或不貪婪的(nongreedy)。 預設是貪婪的,也就是它們會抓出符合樣式最多字元。 在運算子後方加上一個`?`字元,則會讓運算子成為不貪婪的。 (這是問號字元(`?`)的重載,因為它也可以代表前面的字元可以出現一次,或完全不出現(0次或1次)) e.g. 當我們想匹配aaa這個字串(`+`代表前面的字元出現1次或多次) /a+/ 這個正規表達式會找到所有3個a字元 ---> 貪婪(greedy) /a+?/ 這個正規表達式只會匹配到一個a字元 ---> 不貪婪的(nongreedy) **** * **預定義字元集合** ![](https://i.imgur.com/E0Yp7JQ.jpg) **** * **分組** 利用括號`()`分組,括號在正規表達式中有兩種重要意義:分組、捕捉(ch 10.4) e.g. /(ab)+/會符合連續出現一次或多次的ab字串。 **** * **多重選項(或)** 利用管道字元(`|`)來表示多重選項。 e.g. /a|b/ 會符合a字元或b字元 e.g. /(ab)+|(cd)+/ 會符合出現一次或多次的ab或cd字串 **** * **回溯參照** 他們是正規表達式中預先進行匹配的區段,並將這個區段匹配後的結果運用在表達式其他的地方。 存取參照後的記號為反斜線(`\`)加上一個代表順序的數字,例如 \1、\2... e.g. /^([dtn])a\1/ 它會符合一個以d、t或n開頭,緊跟著一個a,再接著第一個捕捉所找到的字元。 若前面經匹配符合的字元是d,則\1即為d。 ```javascript! let str = "dadhbc"; let regex = /^([dtn])a\1/; str.match(regex); // ['dad', 'd', index: 0, input: 'dadhbc', groups: undefined] ``` [註] 上例除了'dad'之外,還有另一個'd',這個'd'是它捕捉到的值,前面有提到說括號`()`有兩種意義,一個是分組,另一個就是捕捉,因此當括號捕捉到該值'd'之後,就可以丟給`\1`,完成回溯參照。 ![](https://i.imgur.com/riJQJcT.jpg) Q: 回溯參照的應用為何? A: 匹配XML e.g. ```javascript! let str = `<strong>whatever</strong>`; let regex = /<(\w+)>(.+)<\/\1>/; str.match(regex); // ['<strong>whatever</strong>', 'strong', 'whatever', index: 0, input: '<strong>whatever</strong>', groups: undefined] ``` ![](https://i.imgur.com/gpp3Pzy.jpg) 看regex101解釋:https://regex101.com/r/dkGcQw/1 如果不用回溯參照,這一點是不可能做到的,因為我們無法預先知道開始標籤相匹配的結束標籤是什麼。 ## ◼ 10.3 編譯正規表達式 正規表達式的處理主要有兩個階段:編譯、執行 1. 編譯階段:發生在建立正規表達式的時候,進行編譯時,JS引擎會讀取表達式的內容,並把它轉換成內部呈現格式(不論那到底是怎樣的格式) 2. 執行階段:發生在使用編譯的正規表達式來對字串做對比時。 在每一次建立正規表達式時,這個讀取與轉換的階段就必須發生一次。因此如果我們能先對複雜的表達是做預先定義,使其觸發預先編譯的動作以供稍後使用,那麼就可以在執行速度上獲得一些明顯的改善。 在10.2.1節中提到JS有兩種方式來建立一個編譯過的正規表達式:透過實值or建構器函式。 #### ▌程式列表10.2 用兩種不同的方式來建立一個編譯過的正規表達式 ```javascript! //flag i: 不區分大小寫 const re1 = /test/i; //透過實值建立一個正規表達式 const re2 = new RegExp("test", "i"); //透過建構器函式建立一個正規表達式 assert(re1.toString() === "/test/i", "Verify the contents of the expression."); assert(re1.test("TesT"), "Yes, it's case-insensitive."); assert(re2.test("TesT"), "This one is too."); assert(re1.toString() === re2.toString(), "The regular expressions are equal."); assert(re1 !== re2, "But they are different objects."); ``` [補充] regExp.test(str) 若括號中的str符合regExp,會回傳true,反之則回傳false。 [重點] 1. 上例中,只編譯正規表達式一次,並把編譯結果儲存在一個變數裡,會是一項重要的效能提升。 2. 每個正規表達式都會是獨一的物件,每當建立一個正規表達式,就會有一個新的正規表達式物件被建立出來。(與其它基本資料型態不同,因為建立正規表達式的結果永遠都是獨一的) 3. 使用建構器函式(new RegExp(...))來建立正規表達式,這種方式讓我們可以用在一個執行期間動態產生的字串,來建立及編譯正規表達式(如下個範例) #### ▌程式列表10.3 編譯一個動態的正規表達式,以便稍後使用 ```javascript! //建立擁有多個元素及多個類別名稱的受測主體 <div class="samurai ninja"></div> <div class="ninja samurai"></div> <div></div> <span class="samurai ninja ronin"></span> <script> function findClassInElements(className, type) { //根據類型來收集HTML元素 //特殊字符 "*" 代表了所有元素 const elems = document.getElementsByTagName(type || "*"); //利用傳入的className來編譯一個正規表達式 const regex = new RegExp("(^|\\s)" + className + "(\\s|$)"); //用於將結果儲存下來 const results = []; for (let i = 0, length = elems.length; i < length; i++){ // 若elems[i].className符合正規表達式,則將elems[i]推入results陣列 if (regex.test(elems[i].className)) { results.push(elems[i]); } } return results; } assert(findClassInElements("ninja", "div").length === 2, "The right amount of div ninjas was found."); assert(findClassInElements("ninja", "span").length === 1, "The right amount of span ninjas was found."); assert(findClassInElements("ninja").length === 3, "The right amount of ninjas was found."); </script> ``` 1. findClassInElements("ninja", "div") ```javascript! function findClassInElements("ninja", "div") { //step1 收集HTML的div元素 const elems = document.getElementsByTagName("div" || "*"); //step2 利用傳入的className"ninja"來編譯一個正規表達式 const regex = new RegExp("(^|\\s)" + "ninja" + "(\\s|$)"); //用於將結果儲存下來 const results = []; for (let i = 0, length = elems.length; i < length; i++){ //step3 若elems[i].className符合正規表達式,則將elems[i]推入results陣列 if (regex.test(elems[i].className)) { results.push(elems[i]); } } return results; } ``` **//step1 收集到的div元素** ```javascript! <div class="samurai ninja"></div> <div class="ninja samurai"></div> <div></div> //elems [div.samurai.ninja, div.ninja.samurai, div] ``` **//step2 利用傳入的className"ninja"來編譯一個正規表達式** 會比對開頭字串是否符合ninja,或在ninja前是否有空白字元,接在ninja之後的需要是空白字元或結尾字串須符合ninja。 [注意] 若把反斜線寫在字串裡,需用連續兩個反斜線(`\\`)進行跳脫。 ```javascript! const regex = new RegExp("(^|\\s)" + "ninja" + "(\\s|$)"); ``` **//step3 若elems[i].className符合正規表達式,則將elems[i]推入results陣列** 可以從step1得知elems抓到了3個div元素,這3個div元素的className分別為"samurai ninja"、"ninja samurai"、""(空的) 將以上3個字串去跟正規表達式做匹配,結果只有"samurai ninja"、"ninja samurai"兩者符合。 "samurai ninja" ![](https://i.imgur.com/bl3EL3W.jpg) "ninja samurai" ![](https://i.imgur.com/fWlUdeX.jpg) **//結果** `findClassInElements("ninja", "div").length === 2` 為true。 ## ◼ 10.4 捕捉符合的區段 正規表達式中的括號`()`有兩種意義:分組、捕捉。 ### ▉ 10.4.1 進行簡單的捕捉 #### ▌程式列表10.4 捕捉內嵌值的一個簡單函式 ```javascript! <div id="square" style="transform:translateY(15px);"></div> <script> function getTranslateY(elem){ //step1 const transformValue = elem.style.transform; if(transformValue){ //step2 const match = transformValue.match(/translateY\(([^\)]+)\)/); //step3 return match ? match[1] : ""; } return ""; } const square = document.getElementById("square"); assert(getTranslateY(square) === "15px", "We've extracted the translateY value"); </script> ``` **//step1** ```javascript! const transformValue = elem.style.transform; //translateY(15px) ``` **//step2** 若字串符合正規表達式,會回傳一個由捕捉值所組成的陣列;如果不符合,則回傳null。 在回傳的陣列裡,第一個元素是符合的整個字串,而各個捕捉到的值,也同樣接連放置在陣列裡。因此從此例來說,索引值為0的資料就是符合的整個字串translateY(15px),而下一筆(索引值為1)的資料就是捕捉值15px(在正規表達式裡,是用括號來定義捕捉)。 ```javascript! const match = transformValue.match(/translateY\(([^\)]+)\)/); //['translateY(15px)', '15px', index: 0, input: 'translateY(15px)', groups: undefined] ``` ![](https://i.imgur.com/syyFxHy.jpg) **//step3** 從這裡可知,我們會取得match[1],即15px。 ```javascript! return match ? match[1] : ""; ``` ### ▉ 10.4.2 使用全域正規表達式做匹配 全域正規表達式,包含了flag g,回傳的結果會有些不同。 #### ▌程式列表10.5 使用match方法進行全域或區域搜尋的差異 ```javascript! <script type="text/javascript"> const html = "<div class='test'><b>Hello</b> <i>world!</i></div>"; //使用區域正規表達式 const results = html.match(/<(\/?)(\w+)([^>]*?)>/); assert(results[0] === "<div class='test'>", "The entire match."); assert(results[1] === "", "The (missing) slash."); assert(results[2] === "div", "The tag name."); assert(results[3] === " class='test'", "The attributes."); //使用全域正規表達式 const all = html.match(/<(\/?)(\w+)([^>]*?)>/g); assert(all[0] === "<div class='test'>", "Opening div tag."); assert(all[1] === "<b>", "Opening b tag."); assert(all[2] === "</b>", "Closing b tag."); assert(all[3] === "<i>", "Opening i tag."); assert(all[4] === "</i>", "Closing i tag."); assert(all[5] === "</div>", "Closing div tag."); </script> ``` **//使用區域正規表達式** 只會找到一筆符合的資料,而在這筆符合結果裡所捕捉到的值,也會出現在回傳的陣列裡。 ```javascript! const results = html.match(/<(\/?)(\w+)([^>]*?)>/); //["<div class='test'>", '', 'div', " class='test'", index: 0, input: "<div class='test'><b>Hello</b> <i>world!</i></div>", groups: undefined] ``` ![](https://i.imgur.com/Ox5DZDE.jpg) **//使用全域正規表達式** 回傳的則是所有符合的結果。 ```javascript! const all = html.match(/<(\/?)(\w+)([^>]*?)>/g); //["<div class='test'>", '<b>', '</b>', '<i>', '</i>', '</div>'] ``` ![](https://i.imgur.com/lEL85If.jpg) 若捕捉值很重要,我們可以改為利用exec()方法來進行全域搜索。此時即可取得捕捉到的值。 #### ▌程式列表10.6 使用exec方法,進行全域搜索與捕捉 ```javascript! const html = "<div class='test'><b>Hello</b> <i>world!</i></div>"; const tag = /<(\/?)(\w+)([^>]*?)>/g; let match, num = 0; while ((match = tag.exec(html)) !== null) { assert(match.length === 4, "Every match finds each tag and 3 captures."); num++; } assert(num === 6, "3 opening and 3 closing tags found."); ``` 試著把每次迴圈的match印出來,可以看見它陣列中第一筆資料會是符合的匹配結果,接續在後方的則是捕捉到的值(因為在正規表達式中有3個括號,代表會捕捉3次) ![](https://i.imgur.com/QIq3fSj.png) ### ▉ 10.4.3 參照捕捉的結果 #### ▌程式列表10.7 使用回溯參照對HTML標籤裡的內容做匹配 ```javascript! const html = "<b class='hello'>Hello</b> <i>world!</i>"; const pattern = /<(\w+)([^>]*)>(.*?)<\/\1>/g; //第一次使用exec let match = pattern.exec(html); assert(match[0] === "<b class='hello'>Hello</b>", "The entire tag, start to finish."); assert(match[1] === "b", "The tag name."); assert(match[2] === " class='hello'", "The tag attributes."); assert(match[3] === "Hello", "The contents of the tag."); //第二次使用exec match = pattern.exec(html); assert(match[0] === "<i>world!</i>", "The entire tag, start to finish."); assert(match[1] === "i", "The tag name."); assert(match[2] === "", "The tag attributes."); assert(match[3] === "world!", "The contents of the tag."); ``` [10.2.2](https://hackmd.io/ZY5j6rpNR0ehFkZFqI-bnQ?both#%E2%96%89-1022-%E8%A9%9E%E5%BD%99%E5%92%8C%E9%81%8B%E7%AE%97%E5%AD%90)最後有提到回溯參照,跟這裡是一樣的概念。 同樣地,陣列中第一筆資料會是符合的匹配結果,接續在後方的則是捕捉到的值。 此外,我們也可以在呼叫replace方法時,在用於進行取代的字串裡取得對捕捉的參照。不同於程式列表10.7中的回溯參照,此舉是使用$1、$2、$3這樣的語法,來對應各個捕捉。 e.g. ```javascript! assert("fontFamily".replace(/([A-Z])/g,"-$1").toLowerCase() === "font-family", "Convert the camelCase into dashed notaion.") ``` ![](https://i.imgur.com/vPi7HvO.jpg) 由於捕捉和分組都是用括號來表示,所以正規表達式處理器完全沒有辦法知道到底哪些括號代表分組,那些括號又代表捕捉。因此它只好把所有括號既視為是捕捉,也視為分組,導致有可能因為在正規表達式裡用括號來分組,而捕捉到比我們預期還多的值。 ### ▉ 10.4.4 非捕捉的分組 ```javascript! const pattern = /((ninja-)+)sword/; ``` 上方的正規表達式,會去檢查使否有一個或多個「ninja-」出現在「sword」的前面,並且我們想捕捉「sword」前面的整個前綴字串。 i.e. 裡面的括號:分組用 / 外面的括號:捕捉用 但事實上,正規表達式處理器並不會這樣理解,造成裡面的括號也同時做捕捉用,將使得實際捕捉到的值會比預期還要多。 因此,根據正規表達式的語法,我們可以在開頭的括號後方加上「?:」,指示這組括號不應該用來進行捕捉,而這就是所謂的「被動式次表達式」(passive subexpression) 於是,我們便可以把上方的正規表達式修正為: ```javascript! const pattern = /((?:ninja-)+)sword/; ``` #### ▌程式列表10.8 不做捕捉的分組括號 ```javascript! const pattern = /((?:ninja-)+)sword/; const ninjas = "ninja-ninja-sword".match(pattern); assert(ninjas.length === 2,"Only one capture was returned."); assert(ninjas[1] === "ninja-ninja-", "Matched both words, without any extra capture."); ``` ninjas回傳的陣列如下: ![](https://i.imgur.com/fSUZ3iG.png) 雖然正規表達式中有兩組括號,但回傳的陣列只會有一個捕捉值。 如果我們不需要捕捉值,就根本不應該進行捕捉。 ## ◼ 10.5 在replace方法裡面使用函式 replace() 第一個參數:一個正規表達式,它會對符合比對的部分作置換 第二個參數:用於取代的值(可以是函式) 以下方為例: ```javascript! "ABCDFfg".replace(/[A-Z]/g,"X") ``` 用X來取代某個字串中的所有大寫字母,因此會得到結果「XXXXXfg」。 第二個參數若以函式作為參數,則如下。 #### ▌程式列表10.9 把一個用橫線做區隔的字串,轉換成駝峰是大小寫 ```javascript! function upper(all,letter) { return letter.toUpperCase(); } assert("border-bottom-width".replace(/-(\w)/g,upper) === "borderBottomWidth", "Camel cased a hyphenated string."); ``` 每找到一筆符合的結果,就會執行第二個參數中的函式一次,執行函式所回傳的結果,就是replace藥用於取代的值。 以上例來說,每次呼叫upper函式,傳入的引數有兩個: 1. 傳入的第一個引數是符合比對的整個字串 2. 傳入的第二個引數是捕捉到的值 因此, 1. 第一次呼叫upper函式:傳給它的引數是「-b」、「b」 2. 第二次呼叫upper函式:傳給它的引數是「-w」、「w」 在每一次呼叫裡,捕捉到的字元(即第二個引數「b」、「w」)會被轉換成大寫(「B」、「W」)回傳回去,並取代掉原先的「-b」、「-w」。 #### ▌程式列表10.10 一種壓縮查詢字串的技巧 假如我們希望可以把foo=1&foo=2&blah=a&blah=b&foo=3轉換成foo=1,2,3&blah=a,b,可以藉助於正規表達式和replace方法。 ```javascript! function compress(source) { const keys = {}; source.replace(/([^=&]+)=([^&]*)/g, function(full, key, value) { keys[key] = (keys[key] ? keys[key] + "," : "") + value; return ""; }); const result = []; for (let key in keys) { result.push(key + "=" + keys[key]); } return result.join("&"); } assert(compress("foo=1&foo=2&blah=a&blah=b&foo=3") === "foo=1,2,3&blah=a,b", "Compression is OK!"); ``` **先討論上半部(keys部分):** ```javascript! const keys = {}; source.replace(/([^=&]+)=([^&]*)/g, function(full, key, value) { keys[key] = (keys[key] ? keys[key] + "," : "") + value; return ""; }); ``` 呼叫function(full, key, value)時,分別傳入3個引數: 1. full:符合比對的整個字串 2. key:第一個捕捉到的值 `([^=&]+)` 3. value:第二個捕捉到的值 `([^&]*)` 從傳入的字串`"foo=1&foo=2&blah=a&blah=b&foo=3"`來看: 1. 第一次呼叫函式:傳給它的引數是「foo=1」、「foo」、「1」 2. 第二次呼叫函式:傳給它的引數是「foo=2」、「foo」、「2」 3. 第三次呼叫函式:傳給它的引數是「blah=a」、「blah」、「a」 4. 第四次呼叫函式:傳給它的引數是「blah=b」、「blah」、「b」 2. 第五次呼叫函式:傳給它的引數是「foo=3」、「foo」、「3」 ![](https://i.imgur.com/fXHAz55.jpg) keys的結果: ![](https://i.imgur.com/KlflNm9.jpg) **討論下半部(result部分):** ```javascript! const result = []; for (let key in keys) { result.push(key + "=" + keys[key]); } return result.join("&"); ``` ![](https://i.imgur.com/GzNOMf8.jpg) ## ◼ 10.6 運用正規表達式來解決常見問題 ### ▉ 10.6.1 匹配換行 #### ▌程式列表10.11 匹配所有文字,包含換行符號 ```javascript! const html = "<b>Hello</b>\n<i>world!</i>"; //第一種 assert(/.*/.exec(html)[0] === "<b>Hello</b>", "A normal capture doesn't handle endlines."); //第二種 assert(/[\S\s]*/.exec(html)[0] === "<b>Hello</b>\n<i>world!</i>", "Matching everything with a character set."); //第三種 assert(/(?:.|\s)*/.exec(html)[0] === "<b>Hello</b>\n<i>world!</i>", "Using a non-capturing group to match everything."); ``` **//第一種** 「`/.*/`」:句號(`.`)是用來匹配除了換行符號之外的所有字元,所以第一種正規表達式,不會匹配到換行符號。 **//第二種** 「`/[\S\s]*/`」:`\S`代表空白字元以外的所有字元、`\s`代表任何空白字元。所以第二種正規表達式代表我們定義了一個集合,會匹配任何空白字元,以及空白字元以外的內容也會被匹配。 **//第三種** 「`/(?:.|\s)*/`」:第三種正規表達式匹配除了換行符號之外的所有字元 或 匹配任何空白字元。 第二、三種,較推薦使用第-4種,因為第二種用了括號,還得使用被動式次表達式(`?:`)。 ### ▉ 10.6.2 匹配Unicode #### ▌程式列表10.12 匹配Unicode ```javascript! const text ="\u5FCD\u8005\u30D1\u30EF\u30FC"; const matchAll = /[\w\u0080-\uFFFF_-]+/; assert(text.match(matchAll), "Our regexp matches unicode!"); ``` `\w`用來包含一般文字字元 `\u0080-\uFFFF`指定了一個範圍,包含所有在U+0080以上的Unicode字元 ### ▉ 10.6.3 匹配跳脫文字 #### ▌程式列表10.13 在css選擇器中,匹配跳脫過的文字 ```javascript! const pattern = /^((\w+)|(\\.))+$/; const tests = [ "formUpdate", "form\\.update\\.whatever", "form\\:update", "\\f\\o\\r\\m\\u\\p\\d\\a\\t\\e", "form:update" ]; for (let n = 0; n < tests.length; n++) { assert(pattern.test(tests[n]), tests[n] + " is a valid identifier"); } ``` 最後一個冒號式特殊字元,沒有做跳脫,因此不會過。 ## ◼ 參考資料 * 忍者JS-Ch10 * [MDN-正規表達式](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Regular_Expressions) * [Wiki-正規表達式](https://zh.wikipedia.org/zh-tw/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F) * [JavaScript: 如何轉換 Regular Expression 變成 RegExp() 的參數](https://magicjackting.pixnet.net/blog/post/208907845-javascript:-%E5%A6%82%E4%BD%95%E8%BD%89%E6%8F%9B-regular-expression-%E8%AE%8A%E6%88%90-regexp()) * [[JS] 正則 表達式(Regular Expression, regex)](https://pjchender.dev/javascript/js-regex/) * [文本分析基礎-正規表達式(RegExp)](https://kopu.chat/%e6%96%87%e6%9c%ac%e5%88%86%e6%9e%90%e5%9f%ba%e7%a4%8e-%e6%ad%a3%e8%a6%8f%e8%a1%a8%e9%81%94%e5%bc%8fregexp/)