# 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}
```