---
# System prepended metadata

title: 重新認識Javascript

---

---
title: '重新認識Javascript'
disqus: Pai
---


# 壹、目錄及代辦事項



## Table of Contents

[TOC]

## 待學習項目
- [ ] - 
- [ ] - 
- [ ] -
- [ ] - 
# 貳、變數與資料型別
## 一、前言
- JavaScript 是個「弱型別」的語言，變數本身無需宣告型別，型別的資訊只在值或物件本身，變數只用來作為取得值或物件的參考

- 所有沒有透過 var 宣告的變數都會自動變成全域變數。
```javascript=
// 對未宣告的變數 m 賦值
m = 1;

console.log(m);  // ok 好，會出現 1
```

## 二、變數的資料型別
變數沒有型別，值才有。

### 1.基本型別: string、number、boolean、null、undefined
### 2. 物件型別: object、function
![](https://i.imgur.com/z06hbKY.png)
#### a. string
- string 字串:字串會用 ' ' (單引號) 或 " " (雙引號) 包夾住，兩者不可混用。
- 如果字串內需用到'可以透過 \ (跳脫字元, escape character) 來處理：
```javascript=
var str = 'Let\'s go!';    // OK
```
- 多行字串時，可以透過 \ (反斜線) 來繼續，要注意的是 \ 反斜線符號後面不能有任何東西，包括空白字元
    
```javascript=
var hello = '這不是一行文 \
這是第二行 \
這是第三行';
```
#### b. number
- 包含整數、小數點、Infinity (無限大) 、 -Infinity (負無限大) ，以及 NaN (不是數值)
- 透過isNaN(value)來判斷一個變數是否為 NaN
```javascript=
isNaN(NaN);       // true

isNaN(123);       // false
isNaN("123");     // false, 因為字串 "123" 可以透過隱含的 Number() 轉型成數字

isNaN("NaN");     // true, 因為字串 "NaN" 無法轉成數字
```
- 當你執行 0.1 + 0.2 === 0.3 //false
- 因為 0.1 以二進位表示會是 0.0001100110011001100110011001100110011001100110011001100... (無限循環)
0.2 以二進位表示會是 0.0011001100110011001100110011001100110011001100110011010
兩者相加後得到 0.010011001100110011001100110011001100110011001100110011
- [解法](https://github.com/nefe/number-precision) 
![](https://i.imgur.com/pkWJrTH.png)

#### c.null 與 undefined
```javascript=
var a;         // undefined, 尚未給值，未定義

var b = null;  // null, 明確代表此變數沒有值

Number( null );         // 0

Number( undefined );    // NaN

Boolean( null );         // false

Boolean( undefined );    // false
```
## 三、物件、陣列以及型別判斷

### 1.物件 Object

#### a.物件建立
```javascript=
var person = {
  name: 'Kuro',
  job: 'Front-end developer',
  sayName: function() {
    alert( this.name );
  }
};
```
#### b.物件讀取
```javascript=
person.name;         // 'Kuro'
person.sayName();    // 'Kuro'

person["name"];         // 'Kuro'
person["sayName"]();    // 'Kuro'
```
- 前者好處是，如果物件的索引鍵剛好是不合法的 JavaScript 的識別字 (如帶有空白的字串或是數字) 時，前者執行就會出現錯誤
```javascript=
var obj = {
  "001": "Hello"
}

obj.001;        //  SyntaxError: Unexpected number

obj["001"];     // "Hello"
```
#### c.屬性新增
```javascript=
var obj = { };
obj.name = 'Object';

obj.name;       // 'Object'
```
#### d.屬性刪除
```javascript=
var obj = { };
obj.name = 'Object';

obj.name;       // 'Object'

delete obj.name;

obj.name;       // 刪除屬性後變成 undefined
```
#### e.判斷屬性是否存在
```javascript=
var obj = {};

console.log( obj.name );      // undefined
```
當該屬性剛好就是 undefined 時，還有 in 運算子 與 hasOwnProperty() 方法
```javascript=
var obj = {
  name: 'Object'
};

// 透過 in 檢查屬性
console.log( 'name' in obj );     // true
console.log( 'value' in obj );    // false

// 透過 hasOwnProperty() 方法檢查
obj.hasOwnProperty('name');       // true
obj.hasOwnProperty('value');      // false
```
### 2.陣列 Array

#### a.新增陣列
```javascript=
var a = [];

a[0] = "apple";
a[1] = "boy";
a[2] = "cat";

a.length;     // 3
//或是
var a = ["apple", "boy", "cat"];
a.length;     // 3
```
- 直接跳過前面的值新增陣列
```javascript=
var array = ['a', 'b', 'c'];
array.length;         // 3

array[7] = 'z';
console.log(array);   // ["a", "b", "c", undefined, undefined, undefined, undefined, "z"]
```
- 在陣列末端新增元素
```javascript=
var array = ['a', 'b', 'c'];
array.length;           // 3

array.push('d');
console.log(array);     // ['a', 'b', 'c', 'd']
```

- 關於更多陣列的[寫法](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Array)
#### b.陣列長度
陣列的長度可以由 ARRAY.length 來取得，length 是可以被覆寫的。
```javascript=
var a = ["apple", "boy", "cat"];
a.length;         // 3

a.length = 1;
console.log(a);   // ["apple"]

a.length = 3;
console.log(a);   // ["apple", undefined, undefined]
```

#### c.如何判斷是否為陣列
```javascript=
Array.isArray([]);            // true
Array.isArray([1]);           // true
Array.isArray(new Array());   // true

Array.isArray();              // false
Array.isArray({});            // false
Array.isArray(null);          // false
Array.isArray(undefined);     // false
```

# 參、JavaScript 是「傳值」或「傳址」?
## 一、基本型別的更新與傳遞:「傳值」 (pass by value)

```javascript=
var a = 10;
var b = a;//b從a複製10的值過來

console.log( a );   // 10
console.log( b );   // 10

a = 100; //當a變成100時

// 變數 b 依然是 10，而變數 a 變成了 100
console.log( a );   // 100
console.log( b );   // 10
```
a只是把宣告當時的值傳給b，之後a和b就各自是獨立了，這個情況稱為「傳值」

## 二、物件型別的更新與傳遞:「傳址」 (pass by reference)

```javascript=
var coin1 = { value: 10 };
var coin2 = coin1;

console.log( coin1.value );       // 10
console.log( coin2.value );       // 10

coin1.value = 100;//當coin1變成100時

console.log( coin1.value );       // 100
console.log( coin2.value );       // coin2也會變成100

console.log( coin1 === coin2 );    // true
```

當建立起一個新的物件時，JavaScript 會在記憶體的某處建立起一個物件 (圖右側)，然後再將這個 coin1 變數指向新生成的物件
當我們宣告了第二個變數 coin2 之後，並且透過 = 將 coin2 指向 coin1 的位置。
當我們更新了 coin1.value 的內容後， coin2.value 的內容也理所當然地被更新了。
![](https://i.imgur.com/EjWQHPR.png)
這種透過引用的方式來傳遞資料，接收的其實是引用的「參考」而不是值的副本時，
我們通常會稱作「傳址」 (pass by reference)。

## 三、Pass by sharing
當funcction內的參數被重新賦值時，外部的內容是不會改變的。
```javascript=
var coin1 = { value: 10 };

function changeValue(obj) {
  obj = { value: 123 };
}

changeValue(coin1);
console.log(coin1);   // 此時 coin1 仍是 { value: 10 }
```
如果不是重新賦值的情況，則又會回到大家所熟悉的狀況：
```javascript=
var coin1 = { value: 10 };

function changeValue(obj) {
  // 僅更新 obj.value，並未重新賦值
  obj.value = 123;
}

changeValue(coin1);
console.log(coin1);   // 此時 coin1 則會變成 { value: 123 }
```

# 肆、運算式與運算子
[MDN](https://developer.mozilla.org/zh-

# 伍、函式 Functions 的基本概念
**函式是物件的一種**
## 一、函數包含三個部分
- 函式的名稱 (也可能沒有名稱，稍後會提到)
- 在括號 ( ) 中的部分，稱為「參數 (arguments) 」，參數與參數之間會用逗號 , 隔開
- 在大括號 { } 內的部分，內含需要重複執行的內容，是函式功能的主要區塊。

## 二、定義函式的方式
### 1. 函式宣告（Function Declaration）
```javascript=
function square(number) {
  return number * number;
}
```
- 透過「函式宣告」方式定義的函式可以在宣告前使用 (函式提升) 
```javascript=
square(2);    // 4

function square(number) {
  return number * number;
}
```
### 2. 函式運算式（Function Expressions）
```javascript=
var square = function (number) {
  return number * number;
};
```
- 透過「函式運算式」定義的函式不可以在宣告前使用：
```javascript=
square(2);    // TypeError: square is not a function

var square = function (number) {
  return number * number;
};
```
- **沒有名字的函式在 JavaScript 是合法的，通常我們會稱它為「匿名函式」**
在函式運算式中，如果想要在 function 後面加上一個名字是可以的
但這個名字只在「自己函式的區塊內」有效
```javascript=
var square = function func(number) {
  console.log( typeof func );   // "function"
  return number * number;
};

console.log( typeof func );     // undefined
```
### 3. 透過 new Function 關鍵字建立函式
```javascript=
var square = new Function('number', 'return number * number');
```
透過 new Function 所建立的函式物件，每次執行時都會進行解析「字串」(如 'return number * number' ) 的動作，運作效能較差，所以通常實務上也較少會這樣做。

### 三、變數的有效範圍 (Scope)
切分變數有效範圍的最小單位是 "function"
```javascript=
var x = 1;

var doSomeThing = function(y) {
  var x = 100;//函式內定義的變數只屬於這個函式內使用，有效範圍我們通常稱它為「Scope」。
  return x + y;
};

console.log( doSomeThing(50) );   // 150
console.log( x );                 // 1


var x = 1;
var doSomeThing = function(y) {
  // 內部找不到 x 就會到外面找，直到全域變數為止。
  // 都沒有就會報錯：ReferenceError: x is not defined
  return x + y;
};

console.log( doSomeThing(50) );   // 51


var x = 1;

var doSomeThing = function(y) {
  x = 100;//這是使用外部的全域變數
  return x + y;
};

console.log( doSomeThing(50) );   // 150
console.log( x );                 // 100
```
### 四、結論
- 變數有效範圍 (scope) 的最小切分單位是 function (ES6 的 let 與 const 例外)
- 即使是寫在函式內，沒有 var 的變數會變成「全域變數」
- 全域變數指的是全域物件 (頂層物件) 的「屬性」

# 陸、透過 DOM API 查找節點
## 一、DOM 節點間的查找遍歷 (Traversing)
- 父子關係：
除了 document 之外，每一個節點都會有個上層的節點，我們通常稱之為「父節點」 (Parent node)，而相對地，從屬於自己下層的節點，就會稱為「子節點」(Child node)。

- 兄弟關係：有同一個「父節點」的節點，那麼他們彼此之間就是「兄弟節點」(Siblings node)。

### 1. Node.childNodes
### 2. Node.firstChild
### 3. Node.lastChild
### 4. Node.parentNode
### 5. Node.previousSibling
### 6. Node.nextSibling

## 二、document.getElementsBy** 與 document.querySelector / document.querySelectorAll 的差異

而 document.getElementsBy** (注意，有個 s) 以及 document.querySelectorAll 則分別回傳 「HTMLCollection」 與 「NodeList」。

這兩者其實是類似的規格實作，「HTMLCollection」只收集 HTML element 節點，而「NodeList」除了 HTML element 節點，也包含文字節點、屬性節點等。 雖然不能使用陣列型別的 method，但這兩種都可以用「陣列索引」的方式來存取內容。

HTMLCollection / NodeList 在大部分情況下是即時更新的，但透過 document.querySelector / document.querySelectorAll 取得的 NodeList 是靜態的。

```javascript=
<div id="outer">
  <div id="inner">inner</div>
</div>

<script>

  // <div id="outer">
  var outerDiv = document.getElementById('outer');

  // 所有的 <div> 標籤
  var allDivs = document.getElementsByTagName('div');

  console.log(allDivs.length);    // 2

  // 清空 <div id="outer"> 下的節點
  outerDiv.innerHTML = '';

  // 因為清空了<div id="outer"> 下的節點，所以只剩下 outer
  console.log(allDivs.length);    // 1
</script>
```
```javascript=
<div id="outer">
  <div id="inner">inner</div>
</div>

<script>

  // <div id="outer">
  var outerDiv = document.getElementById('outer');

  // 所有的 <div> 標籤
  var allDivs = document.querySelectorAll('div');

  console.log(allDivs.length);    // 2

  // 清空 <div id="outer"> 下的節點
  outerDiv.innerHTML = '';

  // document.querySelector 回傳的是靜態的 NodeList，不受 outerDiv 更新影響
  console.log(allDivs.length);    // 2
</script>

```
# 柒、DOM Node 的建立、刪除與修改
## 一、DOM 節點的新增
```javascript=
var newDiv = document.createElement('div');

newDiv.id = "myNewDiv";
newDiv.className = "box";

// 建立 textNode 文字節點
var textNode = document.createTextNode("Hello world!");

// 透過 newDiv.appendChild 將 textNode 加入至 newDiv
newDiv.appendChild(textNode);
```

## 二、document.createDocumentFragment()
透過操作 DocumentFragment 與直接操作 DOM 最關鍵的區別在於 DocumentFragment 不是真實的 DOM 結構，所以說 DocumentFragment 的變動並不會影響目前的網頁文件，也不會導致回流（reflow）或引起任何影響效能的情況發生。
```javascript=
// 取得外層容器 myList
var ul = document.getElementById("myList");

// 建立一個 DocumentFragment，可以把它看作一個「虛擬的容器」
var fragment = document.createDocumentFragment();

for (var i = 0; i < 3; i++){
  // 生成新的 li，加入文字後置入 fragment 中。
  let li = document.createElement("li");
  li.appendChild(document.createTextNode("Item " + (i+1)));
  fragment.appendChild(li);
}

// 最後將組合完成的 fragment 放進 ul 容器
ul.appendChild(fragment);
```

## 三、DOM 節點的修改與刪除

### 1.NODE.appendChild(childNode)

透過 appendChild() 方法，可以將指定的 childNode 節點，加入到 Node 父容器節點的末端

```javascript=
<ul id="myList">
  <li>Item 01</li>
  <li>Item 02</li>
  <li>Item 03</li>
</ul>

<script>
  // 取得容器
  var myList = document.getElementById('myList');
  // 建立新的 <li> 元素
  var newList = document.createElement('li');

  // 建立 textNode 文字節點
  var textNode = document.createTextNode("Hello world!");

  // 透過 appendChild 將 textNode 加入至 newList
  newList.appendChild(textNode);

  // 透過 appendChild 將 newList 加入至 myList
  myList.appendChild(newList);
</script>
```

### 2.NODE.insertBefore(newNode, refNode)
insertBefore() 方法，則是將新節點 newNode 插入至指定的 refNode 節點的前面
```javascript=
<ul id="myList">
  <li>Item 01</li>
  <li>Item 02</li>
  <li>Item 03</li>
</ul>

<script>
  // 取得容器
  var myList  = document.getElementById('myList');

  // 取得 "<li>Item 02</li>" 的元素
  var refNode = document.querySelectorAll('li')[1];

  // 建立 li 元素節點
  var newNode = document.createElement('li');

  // 建立 textNode 文字節點
  var textNode = document.createTextNode("Hello world!");
  newNode.appendChild(textNode);

  // 將新節點 newNode 插入 refNode 的前方
  myList.insertBefore(newNode, refNode);
<script>
```

### 3.NODE.replaceChild(newChildNode, oldChildNode)

replaceChild() 方法，則是將原本的 oldChildNode 替換成指定的 newChildNode。

```javascript=
<ul id="myList">
  <li>Item 01</li>
  <li>Item 02</li>
  <li>Item 03</li>
</ul>

<script>
  // 取得容器
  var myList  = document.getElementById('myList');

  // 取得 "<li>Item 02</li>" 的元素
  var oldNode = document.querySelectorAll('li')[1];

  // 建立 li 元素節點
  var newNode = document.createElement('li');

  // 建立 textNode 文字節點
  var textNode = document.createTextNode("Hello world!");
  newNode.appendChild(textNode);

  // 將原有的 oldNode 替換成新節點 newNode
  myList.replaceChild(newNode, oldNode);
<script>
```

### 4.NODE.removeChild(childNode)
removeChild 方法，則是將指定的 childNode 子節點移除。

```javascript=
<ul id="myList">
  <li>Item 01</li>
  <li>Item 02</li>
  <li>Item 03</li>
</ul>

<script>
  // 取得容器
  var myList  = document.getElementById('myList');

  // 取得 "<li>Item 02</li>" 的元素
  var removeNode = document.querySelectorAll('li')[1];

  // 將 myList 下的 removeNode 節點移除
  myList.removeChild(removeNode);
<script>
```
### 5.事件監聽 EventTarget.addEventListener()與removeEventListener
透過 removeEventListener() 解除事件的時候，第二個參數的 handler 必須要與先前在 addEventListener() 綁定的 handler 是同一個「實體」。

```javascript=
var btn = document.getElementById('btn');

// 把 event handler 拉出來
var clickHandler = function(){
  console.log('HI');
};

btn.addEventListener('click', clickHandler, false);

// 移除 clickHandler， ok!
btn.removeEventListener('click', clickHandler, false);
```

# 柒、那些你知道與不知道的事件們

## 一、介面相關事件
- load 事件：
註冊在 window 物件上，指的是網頁資源 (包括CSS、JS、圖片等) 全數載入完畢後觸發。如果是 img 元素的 load 事件，則表示是此圖片載入完畢後觸發。

- unload 、 beforeunload 事件：
與 load 事件相反，unload 與 beforeunload 事件分別會在離開頁面或重新整理時觸發，而 beforeunload 會跳出對話框詢問使用者是否要離開目前頁面。

- error 事件：
error 事件會在 document 或是圖片載入錯誤時觸發。
值得一提的是，由於維護性的考量，大多事件的註冊我會強烈建議使用「非侵入式 JavaScript」的寫法，另外寫在 <script> 標記，只有 error 事件最適合以 on-event handler 的寫法來處理：
```javascript=
<img src="image.jpg" onerror="this.src='default.jpg'">
```

- resize 事件：當瀏覽器 (window) 或指定元素 (element) 的「尺寸變更」時觸發。

- scroll 事件：當瀏覽器 (window) 或指定元素 (element) 的「捲軸被拉動」時觸發。

- DOMContentLoaded 事件：
類似於 load 事件，但不同的是，load 事件是在網頁「所有」資源都已經載入完成後才會觸發，而 DOMContentLoaded 事件是在 DOM 結構被完整的讀取跟解析後就會被觸發，不須等待外部資源讀取完成。

```javascript=
<head>
  <script>
    document.addEventListener("DOMContentLoaded", function() {
      // 當 document 結構已解析完成才會執行
      document.getElementById('hello').textContent = 'Hello';
    }, false);
  </script>
</head>
```

![](https://i.imgur.com/9yq5x5t.png)

## 二、滑鼠相關事件

- mousedown / mouseup 事件: 這兩個事件分別會在滑鼠點擊了某元素「按下」(mousedown) 按鈕，以及「放開」(mouseup) 按鈕時觸發。

- click 事件: 當滑鼠「點擊」了某元素時觸發。

- dblclick事件: 當滑鼠「連點兩次」了某元素時觸發。

- mouseenter / mousemove / mouseleave 事件：
  - 當滑鼠游標移入了某元素時，會先觸發 mouseenter 事件。
  - 滑鼠游標在這個元素內「移動」時，會連續觸發 mousemove 事件。
  - 直到滑鼠游標離開了這個元素，才觸發 mouseleave 事件。
## 三、鍵盤相關事件
鍵盤相關事件有下列三種，在大多數情況下會將鍵盤事件註冊在 input 的輸入框上。

- keydown 事件: 「壓下」鍵盤按鍵時會觸發 keydown 事件。

- keypress 事件: 除了 Shift, Fn, CapsLock 這三種按鍵外按住時會觸發，若按著不放則會連續觸發。

- keyup 事件: 「放開」鍵盤按鍵時觸發。

[keyCode](https://gist.github.com/tylerbuchea/8011573)的對應表
## 四、表單相關事件
- input 事件： 當 input、 textarea 以及帶有 contenteditable 的元素內容被改變時，就會觸發 input 事件。

- change 事件： 當 input、select、textarea、radio、checkbox 等表單元素被改變時觸發。 但與 input 事件不同的是，input 事件會在輸入框輸入內容的當下觸發，而 change 事件則是在目前焦點離開輸入框後才觸發。

- submit 事件：當表單被送出時觸發，通常表單驗證都會在這一步處理，若驗證未通過則 return false;。

- focus 事件：當元素被聚焦時觸發。

- blur 事件：當元素失去焦點時觸發。
## 五、特殊事件
- compositionstart: 輸入框內開啟輸入法，且正在拼字時觸發。

- compositionupdate: 輸入框內開啟輸入法，且正在拼字或選字時更改了內容時觸發。

- compositionend: 輸入框內開啟輸入法，拼字或選字完成，正要送出至輸入框時觸發。

## 六、自訂事件
自訂事件可以用 Event constructor 建立，同樣透過 addEventListener 去監聽，由 dispatchEvent 決定觸發的時機。

```javascript=
var event = new Event('build');

// 監聽事件
elem.addEventListener('build', function (e) { ... }, false);

// 觸發事件
elem.dispatchEvent(event);
```
若是想要在自訂事件內增加更多資料，則可以改用 CustomEvent：
```javascript=
var event = new CustomEvent('build', { 'detail': elem.dataset.time });
```
# 捌、函式裡的「參數」
## 一、arguments 物件

Arguments特性:
Arguments 物件僅能夠在函數體內使用，它僅作為函數體的一個私有成員而存在。

### 1. argument[i]、argument.length
我們代入的參數數量超過了先前定義好的參數數量，那麼多餘的參數我們有辦法可以取得嗎?
```javascript=
var plus = function (numA, numB) {
  console.log( arguments.length );
  for( var i = 0; i < arguments.length; i++ ){
    console.log( arguments[i] );
  }
  return numA + numB;
};

// 因為有 5 個參數，會先 log 出 5，
// console.log 印出 1 2 3 4 5
// 然後回傳 1+2 的結果
plus(1, 2, 3, 4, 5);
```
### 2. arguments.callee、arguments.callee
- callee，指的是目前執行的函式
- caller，用來取得call該function的來源物件。
```javascript=
function method(a, b, c) {
     console.log(arguments.callee);
     //取得arguments所處的function => method
     console.log(arguments.caller);
     //取得呼叫某function的來源物件，因arguments不是function因此輸出為=> undefined
     console.log(arguments.callee.caller);
     //取得arguments所處的function => method，然後在取得呼叫method的function因此輸出為=> callmethod
     console.log('宣告參數長度--'+arguments.callee.length);
     //為取得arguments所處的function => method，然後取得它所宣告的參數長度 => 3
     console.log('實際參數長度--'+arguments.length);
     //為取得arguments裡的參數長度。=> 2
     console.log('callmethod的參數長度--' + arguments.callee.caller.length)
     //取得arguments所處的function => method，然後在取得呼叫method的function => callmethod，最後在取得callmethod的宣告參數長度因此輸出為 => 1
     console.log(a);//1
     console.log(b);//3
     console.log(c);//undefined
}
function callmethod(a)
{
     method(1,3);
}
callmethod();
```

## 二、以「物件」作為參數
```javascript=
let person = {
  name: 'Anna',
  age: 56,
  job: { company: 'Tesco', title: 'Manager'}
};

function greetWithSpreadOperator ({age, name, job: {company, title}}) {
  var yearOfBirth = 2018 - age;
  console.log(`${ name } works at ${ company } as ${title} and was born in ${ yearOfBirth }.`);
}

greetWithSpreadOperator(person)
//Anna works at Tesco as Manager and was born in 1962.
```

## 三、參數的預設檢查
要是函式其中一個值為undefined，則會造成整個傳出值變成undefined
如:
```javascript=
var plus = function (numA, numB) {
  return numA + numB;
};
plus(1);    // NaN
```
這時就可以運用||or的特性來做預防檢查
```javascript=
var plus = function (numA, numB) {
  numA = numA || 0;
  numB = numB || 0;

  return numA + numB;
};
```
```javascript=
var plus = function (numA, numB) {
  numA = (typeof numA !== 'undefined') ? numA : 0;
  numB = (typeof numB !== 'undefined') ? numB : 0;

  return numA + numB;
};
```
另外，在 ES6 之後，我們也可以像這樣替參數指定預設值：
```javascript=
var plus = function (numA = 0, numB = 0) {
  return numA + numB;
};
```
# 玖、Callback Function 與 IIFE
## 一、Callback Function
- 把函式當作另一個函式的參數，透過另一個函式來呼叫它
```javascript=
window.setTimeout( function(){ ... }, 1000);
//當經過一秒後，要重新呼叫一次function
```

- 控制多個函式間執行的順序
```javascript=
// 為了確保先執行 funcA 再執行 funcB
// 我們在 funcA 加上 callback 參數
var funcA = function(callback){
  var i = Math.random() + 1;

  window.setTimeout(function(){
    console.log('function A');

    // 如果 callback 是個函式就呼叫它
    if( typeof callback === 'function' ){
      callback();
    }

  }, i * 1000);
};

var funcB = function(){
  var i = Math.random() + 1;

  window.setTimeout(function(){
    console.log('function B');
  }, i * 1000);
};

// 將 funcB 作為參數帶入 funcA()
funcA( funcB );
```

## 二、立即被呼叫的函式 (Immediately Invoked Function Expression, IIFE)
題目是這樣的：假設想透過迴圈 + setTimeout 來做到，在五秒鐘之內，每秒鐘依序透過 console.log 印出： 0 1 2 3 4
```javascript=
// 假設想透過迴圈 + setTimeout 來做到
// 每秒鐘將 i 的值 console 出來

for( var i = 0; i < 5; i++ ) {
  window.setTimeout(function() {
    console.log(i);
  }, 1000);
}
//結果會是每隔一秒出現4一次
```
使用IIFE後的正確寫法
```javascript=
for( var i = 0; i < 5; i++ ) {

  (function(x){
    // 將原本的 1000 改成 1000 * x
    window.setTimeout(function() {
      console.log(x);
    }, 1000 * x);
  })(i);

}
```
在ES6改版後，新增了 let 與 const，原本切分變數scope是 "function"，改以外層 { } 作為它的 Scope。
換句話說，將範例中的 var 改為 let 就可以做到保留 i 在執行迴圈當下的「值」的效果：
```javascript=
for( let i = 0; i < 5; i++ ) {
  window.setTimeout(function() {
    console.log(i);
  }, 1000*i);
}
```
# 拾、閉包 Closure

範圍鏈是在函式被定義的當下決定的，不是在被呼叫的時候決定。 所以即使我們在 Global 層透過 innerFunc() 去呼叫內部的 inner()，實際上取得的 msg 仍然是內層的 "local"。
```javascript=
var msg = "global."

function outer() {
  var msg = "local."

  function inner() {
    return msg;
  }

  return inner;
}

var innerFunc = outer();
var result = innerFunc();

console.log( result ); //local
```
當內部 (inner) 函式被回傳後，除了自己本身的程式碼外，也可以取得了內部函式「當時環境」的變數值，記住了執行當時的環境，這就是「閉包」。


## 透過閉包減少全域變數的宣告
### 1.「沒有使用閉包」
因為得宣告全域變數才能讓count一一去做累加，但是，要是當我們的程式碼開始變多了，過多的全域變數會造成不可預期的錯誤，像是你與同事間的變數名稱衝突、沒用到的變數無法回收等等的。
```javascript=
var count = 0;

function counter(){
  return ++count;
}

console.log( counter() );   // 1
console.log( counter() );   // 2
console.log( counter() );   // 3
```
### 2.「使用閉包」
像這樣，我們把 count 封裝在 counter() 當中，不但可以讓裡面的 count 不會暴露在 global 環境造成變數衝突，也可以確保內部 count 被修改。
```javascript=
function counter(){
  var count = 0;

  return function(){
    return ++count;
  }
}

var countFunc = counter();

console.log( countFunc() );   // 1
console.log( countFunc() );   // 2
console.log( countFunc() );   // 3

//可以透過countFunc2宣告一次count，此時你就會發現 countFunc 與 countFunc2 分別是「獨立」的計數器實體，彼此不會互相干擾！
var countFunc2 = counter();

console.log( countFunc2() );   // 1
console.log( countFunc2() );   // 2
```