# JS-4 ## 正則(Regular Expression) > - 對 string 操作的一套規則, 用該規則來過濾 string > - JS 的正則是一個對象 > - 正則作用: > - 匹配 > - 提取 > - 替換 > - 一堆地方都有支援正則 ### 創建正則 > - RegExp > `var 變量 = new RegExp(/表達式/[, '參數'])` > - `/表達式/` 也可以用 string 寫, 只是要注意轉譯 > - `/\d+/` => `'\\d+'` > - 字面量 > `var 變量 = /表達式/[參數];` > #### 區別 ```javascript= // 目標, 驗證是否為數字 let rg = new RegExp('^\d+$'); // \ 轉譯了d, 可是轉譯 d 沒有特別含義, // 所以整體意思是至少一個 d console.log(rg.test('\\d')); // false console.log(rg.test('\d+')); // false console.log(rg.test('d')); // true console.log(rg.test('ddd')); // true console.log(rg.test('9')); // false console.log('--------------'); rg = new RegExp('^\\d+$'); // \ 轉譯了 \, 讓 \ 變成普通字符, // 而 \d 普通字符組合再一起表示 0-9 console.log(rg.test('\\d')); // false console.log(rg.test('\d+')); // false console.log(rg.test('d')); // false console.log(rg.test('9')); // true console.log('--------------'); rg = /^\d+$/; // 寫在 // 裡的, 就直接是普通字符了(元字符) console.log(rg.test('\\d')); // false console.log(rg.test('\d+')); // false console.log(rg.test('d')); // false console.log(rg.test('9')); // true console.log('--------------'); // 正則使用變量的值, 只能用 new, 因為 // 沒辦法拼接 let str = 'abc'; let rg = new RegExp('^[' + str + ']+$'); console.log(rg.test('aaa')); console.log(rg.test('bbb')); console.log(rg.test('ccc')); ``` ### 匹配 `regexObj.test(text)` > - `regexObj`: 正則對象 > - 檢測文本是否符合正則, 返回 Boolean ```javascript= var rg = new RegExp(/123/); var rg2 = /abc/; // 正則表達式不要加 '' console.log(rg.test(123)); // true console.log(rg.test('abc')); // false console.log(rg2.test('abc')); // true console.log(rg2.test(123)); // false ``` ### 邊界符 > - `^` : 開頭 > - `$` : 結尾 ```javascript= var rg = /^abc/; // 必須是 abc 開頭 console.log(rg.test('abcd')); // true console.log(rg.test('aabcd')); // false var rg2 = /abc$/; // 必須是 abc 結尾 console.log(rg.test('abcc')); // false console.log(rg.test('aabc')); // true ``` ### 字符 > #### `[]` : 列舉 > - `[a-z]`: `-` 範圍符 > - `[^a]`: `^` 取反 > - `[a-zA-Z]`: 組合直接接著寫, 不加任何符號 ```javascript= var rg = /^[abc]/; // a || b || c 開頭的都可以 console.log(rg.test('a123')); // true console.log(rg.test('b123')); // true console.log(rg.test('c123')); // true rg = /^[abc]$/; // a || b || c 開頭且結尾, 就是三選一的意思啦 console.log(rg.test('ab')); // false console.log(rg.test('a')); // true console.log(rg.test('b')); // true console.log(rg.test('c')); // true // 範圍列舉 rg = /^[a-z]$/; // a 到 z 選一 console.log(rg.test('a')); // true console.log(rg.test('A')); // false console.log(rg.test(1)); // false // 字符組合 rg = /^[a-zA-Z-_]$/; console.log(rg.test('a')); // true console.log(rg.test('A')); // true console.log(rg.test(1)); // false console.log(rg.test('-')); // true console.log(rg.test('_')); // true // 列舉取反 rg = /^[^a-zA-Z-_]$/; console.log(rg.test('a')); // false console.log(rg.test('A')); // false console.log(rg.test(1)); // true console.log(rg.test('-')); // false console.log(rg.test('_')); // false ``` ### 量詞 > - `*` : 0 到無限次 > - `+` : 1 到無限次 > - `?` : 0 跟 1 > - `{n}` : =n 次 > - `{n,}` : >=n 次 > - `{n,m}` : >=n && <=m 次 > - 中間不要有空格 ```javascript= // * 0 ~ 無限多次 var rg = /^a*$/; // 0 ~ 無限多次的 a console.log(rg.test('')); // true console.log(rg.test('a')); // true console.log(rg.test('aaa')); // true // + 至少一次 rg = /^a+$/; // 1 ~ 無限多次的 a console.log(rg.test('')); // false console.log(rg.test('a')); // true console.log(rg.test('aaa')); // true // ? 0 || 1 次 rg = /^a?$/; // 0 || 1 次的 a console.log(rg.test('')); // true console.log(rg.test('a')); // true console.log(rg.test('aaa')); // false // {n} n 次 rg = /^a{3}$/; // 三次 a console.log(rg.test('')); // false console.log(rg.test('a')); // false console.log(rg.test('aaa')); // true console.log(rg.test('aaaa')); // false // {n,} n 次以上 rg = /^a{3,}$/; // 三次 a 以上 console.log(rg.test('')); // false console.log(rg.test('a')); // false console.log(rg.test('aaa')); // true console.log(rg.test('aaaa')); // true // {n,m} n 次以上, m 次以下 rg = /^a{3,5}$/; // 三次 a 以上 && 五次 a 以下 console.log(rg.test('')); // false console.log(rg.test('a')); // false console.log(rg.test('aaa')); // true console.log(rg.test('aaaa')); // true console.log(rg.test('aaaaa')); // true console.log(rg.test('aaaaaa')); // false // {n,m} 中間不要有空格 rg = /^a{3, 5}$/; console.log(rg.test('aaa')); // false console.log(rg.test('aaaa')); // false console.log(rg.test('aaaaa')); // false ``` ### 小項目: 驗證帳號 ```htmlmixed= <body> <input type="text"/><span>輸入帳號(a-z, A-Z, 0-9, _, - 組合 6~10碼)</span> <script src='test.js'></script> </body> ``` ```javascript= var input = document.querySelector('input'); var span = document.querySelector('span'); var rg = /^[a-zA-Z0-9_-]{6,10}$/; input.onblur = function () { if (rg.test(input.value)) { span.innerText = '符合規定'; span.style.color = 'green'; } else { span.innerText = '請符合(a-z A-Z 0-9 _ - 組合6-10碼)' span.style.color = 'red'; } } ``` ### 括號整理 > - `[]` : 列舉 ```javascript= // [] 裡, 一般情況下都是普通字符 let rg = /^[.+]$/; console.log(rg.test('.')); // true console.log(rg.test('+')); // true console.log(rg.test('...')); // false // 預定義字符為特殊情況 rg = /^[\d]$/; console.log(rg.test('\\')); // false console.log(rg.test('d')); // false console.log(rg.test('9')); // true rg = /^[\\d]$/; console.log(rg.test('\\')); // true console.log(rg.test('d')); // true console.log(rg.test('9')); // false // 特殊情況 [-] : 範圍符 // 特殊情況 [^] : 取反 // []裡, 不存在多位數 let rg = /^[10-29]$/; // 1 或 0-2 或 9 console.log(rg.test('1')); // true console.log(rg.test('0')); // true console.log(rg.test('2')); // true console.log(rg.test('9')); // true console.log(rg.test('19')); // false rg = /^[(10-29)]$/; // 優先級不是特殊情況, 在裡面也是一般字符 console.log(rg.test('19')); // false console.log(rg.test('(')); // true console.log(rg.test(')')); // true rg = /^[\(10-29\)]$/; // 即使轉譯也一樣, // \( 的確有轉譯, 不過 \( 沒有別的意思, 所以還是 ( console.log(rg.test('19')); // false console.log(rg.test('(')); // true console.log(rg.test(')')); // true console.log(rg.test('\\')); // false -> 證明 \ 還是執行了轉譯功能 ``` > - `{}` : 次數 > - `()` : > - 改變優先級 > - 分組引用 > - 分組捕獲 ```javascript= var rg = /^abc{3}$/; // c 重複三次 console.log(rg.test('abcabcabc')); // false console.log(rg.test('abccc')); // true // () 優先級 rg = /^(abc){3}$/; console.log(rg.test('abcabcabc')); // true console.log(rg.test('abccc')); // false ``` ### 預定義字符 > - `\d` : `[0-9]` > - `\D` : `[^0-9]` > - `\w` : `[a-zA-Z0-9_]` > - 注意 ! 沒有 `-` > - `\W` : `[^a-zA-Z0-9_]` > - `\s` : `[\t\r\n\v\f]` > - `\S` : `[^\t\r\n\v\f]` > - `\n` : 換行符 > - `\t` : tab符 > - `.` : 除換行符(`\r` `\n`)以外的任意字符 > - 沒有數量, 預設應該只有一 ```javascript= var rg = /^\d$/; console.log(rg.test(123)); // false -> 數量只有一 console.log(rg.test(1)); // true console.log(rg.test('1')); // true console.log(rg.test('a')); // false rg = /^\D$/; console.log(rg.test(1)); // false console.log(rg.test('a')); // true rg = /^\w$/; console.log(rg.test(1)); // true console.log(rg.test('a')); // true console.log(rg.test('A')); // true console.log(rg.test('_')); // true console.log(rg.test('-')); // false rg = /^\W$/; console.log(rg.test(1)); // false console.log(rg.test('a')); // false console.log(rg.test('A')); // false console.log(rg.test('_')); // false console.log(rg.test('-')); // true rg = /^\s$/; console.log(rg.test(' ')); // true console.log(rg.test('\t')); // true console.log(rg.test('\r')); // true console.log(rg.test('\n')); // true console.log(rg.test('\v')); // true console.log(rg.test('\f')); // true rg = /^\S$/; console.log(rg.test(' ')); // false console.log(rg.test('\t')); // false console.log(rg.test('\r')); // false console.log(rg.test('\n')); // false console.log(rg.test('\v')); // false console.log(rg.test('\f')); // false let rg = /./; console.log(rg.test('a')); // true console.log(rg.test(1)); // true console.log(rg.test('\t')); // true console.log(rg.test('\r')); // false console.log(rg.test('\n')); // false console.log(rg.test('\f')); // true console.log(rg.test('\v')); // true ``` ```javascript= let rg = /^\n$/; console.log(rg.test(` `)); // true rg = /^\t$/; console.log(rg.test(' ')); // true console.log(rg.test(' ')); // false -> 我用空格打的 ``` ### 小練習: 簡易驗證 ```htmlmixed= <body> <input id="tel" type="text"/><span>輸入電話(區域碼)-(電話)</span><br/> <input id="ac" type="text"/><span>輸入帳號(A-Z,a-z,0-9,-,_)(6-10碼)</span><br/> <input id="psw" type="text"/><span>輸入密碼(格式與帳號相同)</span><br/> <input id="pswCF" type="text"/><span>確認密碼</span><br/> <script src='test.js'></script> </body> ``` ```javascript= var tel = document.querySelector('#tel'); var ac = document.querySelector('#ac'); var psw = document.querySelector('#psw'); var pswCF = document.querySelector('#pswCF'); var telRG = /^0\d-\d{7,8}$/; var acRG = /^[\w|-]{6,10}$/; var pswRG = /^[\w|-]{6,10}$/; function rglExp(elem, rgl) { var span = elem.nextElementSibling; elem.onblur = function () { if (rgl.test(this.value)) { span.style.color = 'green'; span.innerText = '格式正確'; } else { span.style.color = 'red'; span.innerText = '格式不正確'; } } } pswCF.onblur = function () { var span = this.nextElementSibling; if (this.value === psw.value) { span.innerText = '密碼一致'; span.style.color = 'green'; } else { span.innerText = '密碼不一致'; span.style.color = 'red'; } } rglExp(tel, telRG); rglExp(ac, acRG); rglExp(psw, pswRG); ``` ### 非英文字符 > - 就是找 unicode 編碼區間, 不過這看起來有點多又有點複雜 > - 我找到的[中文區間](https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block)) > - `\u4E00-\u9FFF` : 包含簡體,繁體,其他語言的漢字(如日文) ```javascript= var rg = /^[\u4E00-\u9FFF]+$/; console.log(rg.test('5566')) // false console.log(rg.test('不能亡')) // true console.log(rg.test('56不能亡')) // false console.log(rg.test('ISad')) // false console.log(rg.test('不 能 亡')) // false ``` ### 正則參數 `/regExp/[參數]` > - 參數 > - `i` : ignoreCase, 忽略大小寫 > - `m` : multiline, 可以進行多行匹配 > - `g` : global, 全局 > - `s` : dotAll, 讓 . 可以匹配換行符(全都匹配) > - 存在 `RexExp.prototype` 裡 ```javascript= console.dir(RegExp.prototype) // global: (...) // ignoreCase: (...) // multiline: (...) ``` > - \*\*\* ```htmlmixed= <body> <input type='text'/> <script src='test.js'></script> </body> ``` ```javascript= var input = document.querySelector('input'); input.onblur = function () { this.value = this.value.replace(/開喜烏龍茶/g, '*****'); } ``` > - 忽略大小寫 ```javascript= // let rg = new RegExp('^[a-z]$', 'i'); // 實例時有兩個參數 let rg = /^[a-z]$/i; // 簡寫 console.log(rg.test('q')); console.log(rg.test('Q')); ``` > - `s` ```javascript= reg = /./ reg.test('\r') // false reg.test('\n') // false reg2 = /./s reg2.test('\r') // true reg2.test('\n') // true ``` ### 其他 > - `''` `[]` `{}` `undefined` `null` 這些我怎麼測都是 false > - `|` : 正則的或 ```javascript= let rg = /^12|34$/; console.log(rg.test('12')); // true console.log(rg.test('34')); // true console.log(rg.test('134')); // true console.log(rg.test('124')); // true console.log(rg.test('1234')); // true console.log(rg.test('9934')); // true console.log(rg.test('1299')); // true // | 會產生複雜的優先級問題, 所以使用 | 時, 經常使用 () rg = /^1(2|3)4$/; // 只能是 124 或 134 rg = /^(12|34)$/; // 只能是 12 或 34 ``` > - `\` : 轉譯符: > - 將普通字符轉成特殊字符: `d` -> `\d`: \[0-9] > - 將特出字符轉成普通字符: `\d` -> `\\d` : \\d ```javascript= // 我要匹配 . 這個符號 // 可是 . 代表除了換行符以外的所有字符 // 運用 \ 轉譯成一般字符 let rg = /\./; console.log(rg.test('.')); // true console.log(rg.test('a')); // false // 匹配 \d rg = /\d/; console.log(rg.test('\d')); // false rg = /\\d/; console.log(rg.test('\d')); // false rg = /\\d/; console.log(rg.test('\\d')); // true -> \ 在 string 也是轉譯 ``` > - `\b` : 一個單詞邊界 > - `\B` : 非單詞邊界 ```javascript= let rg = /ha\b/ console.log(rg.test('halaluya')); // false -> ha 不是在單詞的邊界 console.log(rg.test('ha laluya')); // true ``` ### 常用的正則 > #### 驗證有效數字 ```javascript= // 1. + - 號 時有時無 [+-]? // 2. 個位 0-9皆可, 10以上之首位不為 0 (\d|([1-9]\d+)) // 3. 小數點時有時無, 有小數點時, 後面必須有數字 (\.\d+)? let rg = /^[+-]?(\d|([1-9]\d+))(\.\d+)?$/; console.log(rg.test('-1')); // true console.log(rg.test('+1')); // true console.log(rg.test('01')); // false console.log(rg.test('0')); // true console.log(rg.test('9')); // true console.log(rg.test('0.')); // false console.log(rg.test('0.08')); // true ``` > #### 驗證密碼 ```javascript= // 1. 密碼有範圍 // 2. 密碼使用大小英文, _, -, ... /* 非正則驗證 function checkPass(val) { if (val.length < 6 || val.length > 10) { alert('格式錯誤'); return } let area = ['a', 'b', '_']; // 自己列可用密碼字符 for (let i = 0; i < val.length; i++) { let char = val[i]; if (!area.includes(char)) { // 註 alert('格式錯誤'); return } } } */ // 註 /* a = [1,2,3,4] (4) [1, 2, 3, 4] a.includes(2) true a.includes(5) false */ // 正則驗證 let rg = /^[\w-]{6,10}$/; console.log(rg.test('ab4')); // false console.log(rg.test('ab_-71dD4')); // true ``` > #### 驗證中文姓名 ```javascript= // 1. 中文 \u4E00-\u9FFF, 有另外一個說法是 \u4E00-\u9FA5 // 2. 2~不知道幾位, 假設五位 {2,5} // 3. 有時候有 ・xxx, 而且不只一次, 假設最多兩次 {0,2} let rg = /^[\u4E00-\u9FFF]{2,5}(・[\u4E00-\u9FFF]{2,5}){0,2}$/; // f12 rg.test('一二·哈哈') // false rg.test('一二・哈哈') // true rg.test('一二') // true ``` > #### 驗證郵箱 ```javascript= let rg = /^\w+(((\.|-)\w+))*@[a-zA-Z0-9]+((\.|-)[a-zA-Z0-9]+)*\.[a-zA-Z0-9]+$/; // aa@gamil.com // aa@yahoo.com.tw // \w+(((\.|-)\w+))* // 帳號 // 1. [a-zA-Z0-9_]開頭, 至少一位 // 2. - | . 不能開頭, 且不能連續 (.. 或 -- 或 .- 或 -.) // 3. - | . 後面要接至少一個 [a-zA-Z0-9_], 不能結尾, 且不一定要有 // @[a-zA-Z0-9]+ // 公司名字/ 郵箱名/ ... // ((\.|-)[a-zA-Z0-9]+)* // 預防有多域名 // 企業郵箱有可能名字使用到 - | . // [a-zA-Z0-9]+ // 域名 (.com/ .tw/ .org/ .edu/ ...) ``` ### 正則的方法 > - `RexExp.prototype` 的方法 > - `test()` 匹配 > - `exec()` 提取 > - `String.prototype` 的方法 > - `match()` > - `replace()` > - `split()` > - `search()` > - 正則捕獲: 正則 與 string 要匹配, 捕獲不到返回 null > #### `test()` > - 返回boolean > - 符合即返回 > #### `exec()` > - 返回的是陣列 > - `index`: 捕獲的內容在字符中的起始索引 > - `input`: 原始字符 > - 如果沒有匹配內容, 返回 null > - 只返回一個結果, 默認只捕獲第一個 ( 正則懶惰性 ) > #### 正則懶惰性 > - 默認 lastIndex 不會修改, 導致每次都是從 0 的位置開始捕獲 > - 解決辦法: 全局修飾符 g ```javascript= // 正則懶惰性 let str = 'GodJJ: 20歲; BeBe: 21歲'; let rg = /\d+/; // lastIndex: 正則下次匹配的起始位置 console.log(rg.lastIndex); // 0 console.log(rg.exec(str)); // ["20", index: 7, input: "GodJJ: 20歲; BeBe: 21歲", groups: undefined] console.log(rg.lastIndex); // 0 console.log(rg.exec(str)); // ["20", index: 7, input: "GodJJ: 20歲; BeBe: 21歲", groups: undefined] // 懶惰性的原因: 默認 lastIndex 不會修改, 導致每次都是從 0 的位置開始捕獲 // 手動改也沒用 rg.lastIndex = 9; console.log(rg.lastIndex); // 9 console.log(rg.exec(str)); // ["20", index: 7, input: "GodJJ: 20歲; BeBe: 21歲", groups: undefined] // 解法: 全局修飾符 g rg = /\d+/g; console.log(rg.lastIndex); // 0 console.log(rg.exec(str)); // ["20", index: 7, input: "GodJJ: 20歲; BeBe: 21歲", groups: undefined] console.log(rg.lastIndex); // 9 // ["21", index: 18, input: "GodJJ: 20歲; BeBe: 21歲", groups: undefined] console.log(rg.lastIndex); // 20 console.log(rg.exec(str)); // null // 全部捕獲後, 再次捕獲返回 null, 且 lastIndex 歸 0 console.log(rg.lastIndex); // 0 console.log(rg.exec(str)); // ["20", index: 7, input: "GodJJ: 20歲; BeBe: 21歲", groups: undefined] // 小心! test 驗證後也會修改 lastIndex str = 'GodJJ: 20歲; BeBe: 21歲'; rg = /\d+/g; if (rg.test(str)) { console.log(rg.lastIndex); // 9 -> test 驗證後修改了 console.log(rg.exec(str)); // ["21", index: 18, input: "GodJJ: 20歲; BeBe: 21歲", groups: undefined] // 導致結果可能不是預期的 } ``` ```javascript= // 一次獲取多個 ;(function () { function execAll(str) { let res = this.exec(str), // 捕獲 arr = []; // 存放捕獲訊息 // 判斷調用的對象有沒有開全局修飾符, 沒開就返回, 因為 // 1. 沒開怎麼調都是第一個 // 2. 沒開的話 this.exec(str) 永遠都有值, 而造成後面 while 的死循環 if (!this.global) { if (res) { // 如果有就返回帶值的陣列 arr.push(res[0]); return arr } else { // 否則返回 null return arr.length === 0? null: arr; // 如果沒值就返回 null } } while (res) { arr.push(res[0]); // 存放訊息 res = this.exec(str); // 捕獲 } return arr } RegExp.prototype.execAll = execAll; })() let str = 'GodJJ: 20歲; BeBe: 21歲'; let rg = /\d+/g; console.log(rg.execAll(str)); // (2) ["20", "21"] rg = /\d+/; console.log(rg.execAll(str)); // ["20"] // 註 let a = /\d/g; let b = /\d/; console.dir(a); // /\d/g // global: true -> 有沒有開全局的訊息在實例裡的 global console.dir(b); // /\d/ // global: false ``` > #### `String.prototype.match()` > - 在全局修飾符g 的前提下, 可以提取多個 > ```javascript= // 提取 age let str = 'GodJJ: 20歲, BeBe: 21歲' let rg = /\d+/g; let age = str.match(rg); console.log(age); // (2) ["20", "21"] // 提取 mail let str = 'aa@gmail.com, bb@yahoo.com.tw' let rg = /\w+@\w+(\.\w+)+/g let mail = str.match(rg); console.log(mail); // (2) ["aa@gmail.com", "bb@yahoo.com.tw"] ``` > #### 分組 > - `()` : 分組 > - `(?:)`: 只分組, 不捕獲 > - 捕獲的 \[0] 為大正則匹配的結果, 後面依分組捕獲 ```javascript= // 假設有個規律字串規律是生日加上編號, 編號可能為 0-9 或 X, X 代表 10 let str = '20200101X'; let rg = /^(\d{4})(\d{1,2})(\d{1,2})(\d|X)$/; console.log(rg.exec(str)); // ["20200101X", "2020", "01", "01", "X", ..] console.log(str.match(rg)); // ["20200101X", "2020", "01", "01", "X", ..] // 捕獲到的 [0] 為 大正則匹配結果 // 後面依序分組捕獲 // 假如我只想要生日訊息, 編號我不要 // rg = /^(\d{4})(\d{1,2})(\d{1,2})\d|X$/; -> | 會產生混亂優先級 // (?:) 只分組 (改變優先級), 不捕獲 rg = /^(\d{4})(\d{1,2})(\d{1,2})(?:\d|X)$/; console.log(rg.exec(str)); // ["20200101X", "2020", "01", "01", ...] console.log(str.match(rg)); // ["20200101X", "2020", "01", "01", ...] ``` > - String.prorotype.match 的問題 > - 全局時, 無法捕獲分組匹配 ```javascript= let str = '@2020-@01-@11'; let rg = /@(\d+)/; console.log(rg.exec(str)); // ["@2020", "2020", ...] console.log(str.match(rg)); // ["@2020", "2020", ...] // 沒開全局時, 兩個都正常捕獲兩項 (大正則與分組) rg = /@(\d+)/g; console.log(rg.exec(str)); // ["@2020", "2020", ...] console.log(rg.exec(str)); // ["@01", "01", ...] console.log(rg.exec(str)); // ["@11", "11", ...] console.log(str.match(rg)); // ["@2020", "@01", "@11", ...] // 開全局時, match 只會捕獲大正則 // 解決辦法: 自己寫 execAll let str = '@2020-@01-@11'; let rg = /@(\d+)/g; let res = rg.exec(str), big = [], small = []; while(res) { big.push(res[0]); small.push(res[1]); res = rg.exec(str); } console.log(big, small); ["@2020", "@01", "@11"] ["2020", "01", "11"] ``` > - 分組引用 > - `\數字`: 內容必須與該分組內容相同 ```javascript= let rg = /[a-zA-Z]([a-zA-Z])\1[a-zA-Z]/; // 第三個英文字必須與第一組(第二個英文字)相同 console.log(rg.test('book')); // true console.log(rg.test('boak')); // false ``` ### 貪婪與非貪婪 > - 貪婪模式: 只要合法, 小孩子才做選擇 > - 量詞默認會開啟貪婪模式 > - 非貪婪模式: 有就好 > - `?` 可以關掉貪婪模式 ```javascript= let rg = /\d+/g; console.log('2020-12-31'.match(rg)); // ["2020", "12", "31"] // 問題: + 是一個以上, 那為何是 2020 12 31, 為何不是 2 0 2 0 1 2 3 1? // 正則捕獲貪婪性: 默認情況是以最長結果來獲取 // 量詞後面設置?: 關閉貪婪模式: 最短匹配 rg = /\d+?/g; console.log('2020-12-31'.match(rg)); // ["2", "0", "2", "0", "1", "2", "3", "1"] ``` ### 其他捕獲方法 > #### `RegExp.$` ```javascript= // 捕獲成功後, 系統會將捕獲結果存至 $ 中 // RegExp.$1 ~ RegExp.$9 最多可以存九組分組訊息 let rg = /@(\d+)/g; let str = '@20-@99-@30'; console.log(rg.test(str)); // true console.log(RegExp.$1); // 20 console.log(RegExp.$2); // console.log(rg.test(str)); // true console.log(RegExp.$1); // 99 console.log(rg.test(str)); // true console.log(RegExp.$1); // 30 console.log(rg.test(str)); // false console.log(RegExp.$1); // 30 rg = /@(\d+)/g; str = '@20-@99-@30'; console.log(rg.exec(str)); // ["@20", "20", ...] console.log(RegExp.$1); // 20 console.log(RegExp.$2); console.log(rg.exec(str)); // ["@99", "99", ...] console.log(RegExp.$1); // 99 console.log(rg.exec(str)); // ["@30", "30", ...] console.log(RegExp.$1); // 30 console.log(rg.exec(str)); // null console.log(RegExp.$1); // 30 // 但 RegExp 是全局作用域下的, 故很容易亂掉, 亦即每次匹配都會覆蓋 $ 的值 rg = /@(\d+)/g; rg2 = /@(\d+)/g; console.log(rg.exec('@20-@99-@30')); console.log(RegExp.$1); // 20 console.log(rg2.exec('@100-@1-@40')); console.log(RegExp.$1); // 100 // rg 要用的值就被蓋掉了 ``` ### 替換 > #### `str.replace(regexp|substr, newSubStr|function)` > - 第一個參數為被替換的 str 或 正則 > - 第二個參數為替換的 str > - 替換後返回一個新str > - 一次只替換一個, 但用正則的 g 可以解決 ```javascript= // 不用正則 let str = 'a@a'; str = str.replace('a', 'b'); console.log(str); // b@a str = str.replace('a', 'b').replace('a', 'b'); console.log(str); // b@b // 用正則 str = 'a@a'; str = str.replace(/a/g, 'b'); console.log(str); // b@b // 不用正則的問題: str = 'a@a'; str = str.replace('a', 'ab').replace('a', 'ab'); console.log(str); // abb@a // 執行完第一次replace -> ab@a -> 然後還是從 [0] 開始找 (懶惰性) -> abb@a // 用正則的g就能解決 str = 'a@a'; str = str.replace(/a/g, 'ab'); // 開 g console.log(str); // ab@ab ``` > - `newSubStr` ```javascript= // 如果參數用字符串時, 且第一個參數用正則 // 此時就可以用 $ 來調用分組匹配 let str = '2020-01-10'; let rg = /^(\d{4})-(\d{1,2})-(\d{1,2})$/; newStr = str.replace(rg, '$1年$2月$3日'); // 調用 $1 $2 $3 組的內容 console.log(newStr); // 2020年01月10日 ``` > - `function` ```javascript= let str = '@@2020-01-10-03-30-29'; let rg = /(\d+)-(\d+)/g; str.replace(rg, function (...args) { console.log(args); // ["2020-01", "2020", "01", 2, "2020-01-10-03-30-29"] // ["10-03", "10", "03", 10, "2020-01-10-03-30-29"] // ["30-29", "30", "29", 16, "2020-01-10-03-30-29"] }) // 1. 匹配幾次就能執行幾次 function // 2. 每次執行都會傳實參進來 // (match, p1, pn, offset, string) // 2.1 第一個參數為匹配的字串 // 2.2 第二-第n 個參數為分組匹配的組別 // 2.3 接著是匹配到的初始位置 // 2.4 被匹配的原字符串 ``` ```javascript= // 改成 x年x月x日 的形式, 且月日不足兩位時補 0 let str = '2020-1-3'; let rg = /(\d+)-(\d+)-(\d+)/; str = str.replace(rg, (...args) => { console.log(args); // ["2020-1-3", "2020", "1", "3", 0, "2020-1-3"] let [,$1,$2,$3] = args; $2.length < 2? '0' + $2: null; $3.length < 2? '0' + $3: null; return $1 + '年' + $2 + '月' + $3 + '日' }) console.log(str); // 2020年1月3日 ``` ```javascript= // 將首字母大寫 let str = 'hello world'; let rg = /\b([a-zA-Z])([a-zA-Z]*)\b/g; str = str.replace(rg, (...args) => { console.log(args); /* 我覺得這個比較方便 let [,$1,$2] = args; return $1.toUpperCase() + $2 */ let [match, $1] = args; return $1.toUpperCase() + match.substring(1) }) console.log(str); ``` ```javascript= // 獲取最多的字符與次數 let str = 'helloWorld'; /* 慢慢找 let obj = {}; // 1. 先把字符遍歷出來 [].forEach.call(str, function (v) { // 1.1 驗證有沒有在 obj 裡, 這些方法都可以 // if (v in obj) { // if (obj.hasOwnProperty(v)) { // 註1. if (typeof obj[v] !== 'undefined') { // 有的話次數加一 obj[v]++; return } // 1.2 沒的話就添加, 次數為 1 obj[v] = 1 }) // console.log(obj); let max = 1, arr = []; // 2. 找到最大數量 for (let k in obj) { if (obj[k] > max) { max = obj[k]; } } // console.log(max); // 3. 找到誰符合最大數量 for (let k in obj) { if (obj[k] === max) { arr.push(k) } } console.log(`出現最多的是 ${arr}, 出現了${max}`); */ /* 排序 let str = 'helloWorldadsfkldjwjaaacjk'; // 1. 先將str 拆成 [] 後排序 (註2), 排序後再組回來 str = str.split('').sort((a,b)=>a.localeCompare(b)).join(''); let rg = /([a-zA-Z])\1+/g; // 寫正則來把相同的組起來, 順便過濾只有一個的 // console.log(str.match(rg)); // ["aaaa", "ddd", "jjj", "kk", "llll", "oo"] // 2. 組起來後再排序, 降序排 let arr = str.match(rg).sort((a,b)=>b.length-a.length); // console.log(arr); // ["aaaa", "llll", "ddd", "jjj", "kk", "oo"] // 3. 開始找最大值, 已經排過序了, 所以第一個一定是最多的 let max = arr[0].length, bigArr = [arr[0].substr(0,1)]; // console.log(max, bigArr, arr); // 3.1 看看有沒有跟第一個一樣多的, 有就推進去 for (let i = 1; i < arr.length; i++) { if (arr[i].length < max) { break } bigArr.push(arr[i].substr(0,1)); } console.log(`出現最多的是 ${bigArr}, 出現了${max}`); // 出現最多的是 a,l, 出現了4 */ // replace let str = 'helloWorldadsfkldjwjaaacjk'; // 1. 先將字串排序 str = str.split('').sort((a,b)=>a.localeCompare(b)).join(''); // console.log(str); let max = 0, bigArr = [], flag = false; for (let i = str.length; i > 0; i--) { // 寫正則來匹配, 整個長度開始往下搜, 成功匹配的那次就是最大次數 // let rg = /([a-zA-Z])\1{i-1}/g -> 沒辦法拼接 let rg = new RegExp('([a-zA-Z])\\1{' + (i-1) + '}', 'g'); // console.log(rg); str.replace(rg, (match, $1)=> { // 匹配成功就開始塞東西 max = i; bigArr.push($1); flag = true; }) if (flag) break // 塞完就跳出 } console.log(`出現最多的是 ${bigArr}, 出現了${max}`); ``` ```javascript= // 註1. Object.prototype.hasOwnProperty() // - obj.hasOwnProperty(prop) // - 查看對想裡有沒有某個屬性 // - let a = {"a":1, 'b':2} // a.hasOwnProperty('a') // true // a.hasOwnProperty('c') // false // 註2. String.prototype.localeCompare() // - referenceStr.localeCompare(compareString[, locales[, options]]) // - 比較 referenceStr 跟 compareString 的位置 // - 如果返回值為負數, 表示 referenceStr 在 compareString 前面 // - 如果返回值為正數, 表示 referenceStr 在 compareString 後面 // - 'a'.localeCompare('b') // -1 // - 'a'.localeCompare('a') // 0 // - 'b'.localeCompare('a') // 1 ``` ```javascript= // 時間字符串格式化 /* 整理前 ;(function () { function formatTime() { // 1. 先把數字拿出來 let timeArr = this.match(/\d+/g); // console.log(timeArr); // ["2020", "01", "01", "06", "23", "41"] // 2. 寫個模板 let temp = '{0}年{1}月{2}日 {3}時{4}分{5}秒'; // 3. 把拿到的數字替換模板的 {} temp = temp.replace(/\{(\d+)\}/g, (...args)=>{ let [,$1] = args; // 3.1 替換的邏輯 // console.log($1); // 0 1 2 3 4 5 // console.log(timeArr[$1]); // 2020 1 1 6 23 41 // 3.2 字串處理 // 3.2.1 沒傳的補 00 let time = timeArr[$1] || '00'; // 3.2.2 只傳一位的補 0 time.length < 2? time = '0' + time: null; // 替換 return time }) // console.log(temp); // 2020年01月01日 06時23分41秒 return temp } String.prototype.formatTime = formatTime; })() */ ;(function () { // 參數: 模板 // 返回: 換好的字串 function formatTime(temp = '{0}年{1}月{2}日 {3}時{4}分{5}秒') { let timeArr = this.match(/\d+/g); return temp.replace(/\{(\d+)\}/g, (...[,$1])=>{ let time = timeArr[$1] || '00'; return time.length < 2? '0' + time: time; }) } function aabbcc() { console.log(this); } // 如果有多個函數要丟到原型時, 可以用這招 // 不過 eval 好像很危險, 而且嚴格模式禁止使用 eval 了, // 等我找到高手再問他們 eval 的細節 // MDN 叫我們改用 Function() 我一直沒成功... ['formatTime', 'aabbcc'].forEach((v)=>{ // String.prototype['formatTime'] = formatTime; String.prototype[v] = eval(v); // 註 }) })() console.log('2020-01-10 04:39:15'.formatTime()); // 2020年01月10日 04時39分15秒 console.log('2020/01/10 04:39:15'.formatTime()); // 2020年01月10日 04時39分15秒 console.log('2020-1-1 4:39:15'.formatTime()); // 2020年01月01日 04時39分15秒 console.log('2020-01-10'.formatTime()); // 2020年01月10日 00時00分00秒 console.log('2020/1/1 0:01:20'.formatTime('{1}月{2}日 {3}時{4}分')); // 01月01日 00時01分 // BUG 的地方, 等我有空再改, 就是沒有給年月日會產生Bug // 因為timeArr ['','',''] 取到三項時分秒的索引在 0 1 2 // 導致 timeArr[3], timeArr[4], timeArr[5] 沒東西而補 0 console.log('4:9:5'.formatTime('{3}時{4}分{5}秒')); // 00時00分00秒 // 測試可不可以用, 正常使用~~ ''.aabbcc(); // String {""} ``` ```javascript= // 註 eval // - eval(string) // - 簡單說就是把字符串的引號給脫了, 執行參數的字符串 // - 如果參數不是傳字符串, 那就直接返回 eval('var a = 10;'); // undefined -> 定義 a a // 10 eval('function tmp(){}'); // undefined -> 定義tmp tmp // ƒ tmp(){} eval(';(function(){return 3})()'); // 3 -> 自調用 eval('[1,2,3]'); // (3) [1, 2, 3] -> 非字符串, 直接返回 // 有個神奇的東西我還是看不懂 let a = 10; function tmp(){ let a = 20; console.log(eval('a')); let b = eval; console.log(b('a')); } tmp() // 20 -> 直接調用時訪問的是局部變量 // 10 -> 間接調用時訪問的是全局變量 // ???????????????????? ``` ```javascript= // 拿到訪問URL的所有參數 // URL // 1. = 前後為 k, v // 2. =?#& 有特殊含義 ;(function () { // 返回一個 dict function queryURLParams() { let obj = {}; // 拿到 = 前後, 取的東西為非特殊涵義字符 this.replace(/([^?&#=]+)=([^?=&#]+)/g, (...[,$1,$2])=>obj[$1]=$2); // HASH 另外找, # 之後為 HASH value this.replace(/#([^=&#?]+)/, (...[,$1])=> obj['HASH']=$1); // console.log(obj); } String.prototype.queryURLParams = queryURLParams; })() 'https://www.google.com/?a=1&b=2#vedio'.queryURLParams() // {a: "1", b: "2", HASH: "vedio"} 'https://www.google.com/?b=2#vedio'.queryURLParams() // {b: "2", HASH: "vedio"} 'https://www.google.com/?a=1&b=2'.queryURLParams() // {a: "1", b: "2"} ``` ```javascript= // 千分符 ;(function () { // 方法一: 整個倒過來慢慢加 // 避免 123,45 的情形, // 543,21 再倒回來即可 function threeComma() { // 倒過來 let num = this.split('').reverse().join(''); // console.log(num); // i = 2 : 54321 取第三個, 所以拿 [2] 的位置 // i += 4 : 因為要再往後三位, 但是多加了一個 ',' 所以是 4 位 // num.lenght - 1 : 因為避免 654,321, 的情形, 讓最後一次不執行 for (let i = 2; i < num.length-1; i += 4) { // 取 [0]~[i] 位, substring 不含第二參, 所以 +1 // substring(0,3) 才有取三個: [0] [1] [2] let prev = num.substring(0, i+1), next = num.substring(i+1); num = prev + ',' + next; } num = num.split('').reverse().join(''); return num } // 用正則 // ! (?=) 為正向預查 // ! (?!) 為負向預查 function threeCommaRg() { return this.replace(/\d{1,3}(?=(\d{3})+$)/g, match=>{ // console.log(match); return match + ',' }) } String.prototype.threeComma = threeComma; String.prototype.threeCommaRg = threeCommaRg; })() console.log('123456789'.threeCommaRg()); console.log('23456789'.threeCommaRg()); console.log('3456789'.threeCommaRg()); console.log('123456789'.threeComma()); console.log('23456789'.threeComma()); console.log('3456789'.threeComma()); ``` ### `(?=)` & `(?!)` > - `(?=)` 正向預查 > - `(?!)` 負向預查 > - 補充匹配的條件用的, 只匹配不捕獲 ```javascript= let rg = /\d{1,3}(?=(\d{3})+$)/g // 在三位數字一組結尾的情況下, 擷取 \d{1,3} ```