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