---
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

#### 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)

#### 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 的內容也理所當然地被更新了。

這種透過引用的方式來傳遞資料,接收的其實是引用的「參考」而不是值的副本時,
我們通常會稱作「傳址」 (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>
```

## 二、滑鼠相關事件
- 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
```