---
# System prepended metadata

title: JS-4

---

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