###### tags: `忍者JavaScript開發技巧探秘` `JavaScript` `讀書會` # 忍者JavaScript開發技巧探秘 -- chapter 5 大師級函式:閉包與範圍 * 何謂閉包(範圍) * 閉包的常見使用方式 * 透過執行背景空間,追蹤函式執行的順序 * 認識字彙環境與巢狀程式,進一步瞭解閉包是如何使用範圍外部的變數 * 瞭解字彙環境的建立順序,進一步明白三種宣告的特性及Hoisting的真相 * 透過上面學到的新概念重新審視閉包的運作原理 ## 5.1 瞭解閉包(範圍) 首先:先複習,函式可以在宣告之後,可以隨時被呼叫 閉包 讓函式能存取或操作函式外部的變數, 具體來說是函式可以存取定義/宣告時的**所屬範圍內**的所有相關的**變數**及其他**函式** 而呼叫函式的時候,會先從宣告時的**所屬範圍內**逐一向外找尋所有相關的**變數**及其他**函式** * 程式列表 5.1 ```javascript= var outerValue = "ninja"; // 在全域宣告一個變數,outerValue function outerFunction() { assert(outerValue === "ninja","I can see the ninja."); } // 在全域宣告一個函式,outerFunction outerFunction(); // 在全域中呼叫outerFunction函式 // 函式內沒有outerValue // 所以向外尋找outerValue // 於是在外部的第一層(全域)中找到outerValue // 並放入assert中做運算 ``` [code pen Nanja ver 2.0 ch.5 Listing 5.1-3](https://codepen.io/nikiweikame/pen/RwJROYx?editors=0010) 閉包的進階狀況 * 程式列表 5.2 ```javascript= var outerValue = "samurai"; var later; // 在全域宣告一個有值的變數outerValue 及 一個空變數later function outerFunction() { // 在全域宣告一個函式outerFunction var innerValue = "ninja"; // 在函式中宣告一個變數innerValue // 這個變數只能在函式內呼叫/存取他 function innerFunction() { // 在函式中宣告另一個變函式innerFunction assert(outerValue === "samurai", "I can see the samurai."); // 從全域取得outerValue值,並執行assert assert(innerValue === "ninja", "I can see the ninja."); // 從函式中取得outerValue值,並執行assert } later = innerFunction; // 改變全域中later的值 } outerFunction(); // 執行函式去改變 later的值 later(); // 在全域呼叫函式later() // 函式innerFunction,透過later在全域中呼叫 // assert的兩個條件句是否會成立 // 第一個assert // 因為outerValue的值在全域 // 所以可以成功取得outerValue的值"samurai" // assert的結果會是pass // 而第二個assert // 理論上在全域中呼叫later // 是無法像innerFunction一樣取得innerValue的值 // 但是結果卻是pass // 原因 // 在outerFunction改變later的值得時候 // 也將定義/宣告innerFunction的閉包(環境)一併賦予給later // 所以不管在哪呼叫later // 都相當於在定義/宣告innerFunction的閉包(環境)中呼叫later ``` * 閉包與函式相依,假如函式是球,閉包就是附加在球上的鍊子 * 閉包與函式有著相同的存活時間,函式消失閉包也會跟著消失 * 閉包可以在函式執行時 支援所有函式所需要的東西(如果閉包有的話) * 雖然沒有一個明確的物件可以檢視閉包的內容,但是閉包存在就會消耗記憶體的空間 ## 5.2 開始使用閉包 ### 5.2.1 摹擬私有變數 ~~用閉包製作出類似私有變數的東西 * 何謂私有變數 在**其他程式語言中**物件的一個屬性,這個屬性對外是隱藏的 所以在使用上,可以不透露這些隱藏屬性運作的細節 進而減少物件使用者的負擔 但是JavaScript不支援這個功能,本章節要展示用閉包做出類似私有變數的功能 * 程式列表 5.3 ```javascript= function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } // 製作一個建構器函式Ninja // 並在裡面宣告一個變數 feints(佯攻) 來記錄狀態 // 這個變數局限於建構器函式內 // 以及兩個method方法 // 一個方法getFeints 會回傳feints的值(讀值器) // 另一個方法feint 會增加feints的值(增值器) var ninja1 = new Ninja(); ninja1.feint(); // 透過 建構器函式Ninja 建立一個物件 ninja1 // 並執行 方法 feint assert(ninja1.feints === undefined, "And the private data is inaccessible to us."); // 確認我們無法訪問 feints // 試圖去透過ninja1去訪問feints, // 但是因為feints並沒有透過建構器函式建立於ninja1上 // 所以無法訪問,訪問的預期結果是undefined // pass assert(ninja1.getFeints() === 1, "We're able to access the internal feint count."); // 但是可以透過ninja1的method getFeints去訪問ninja1的feints // 經過method feint 增加 feints的值 之後 預期的結果會是 1 // pass var ninja2 = new Ninja(); // 透過 建構器函式Ninja 建立另一個物件 ninja2 assert(ninja2.getFeints() === 0, "The second ninja object gets it’s own feints variable."); // 透過ninja1的method getFeints去訪問ninja2的feints ``` 疑問ninja1.getFeints() 跟 ninja2.getFeints() 都是 return feints feints值為什麼不相等 訪問的對象是ninja1/2的 method,而不是建構器函式Ninja 上一章說到new是建立一個物件,然後在物件中執行建構器函式 也就是說我們在新建立的物件中宣告了一個變數及兩個function 既然宣告的函式,當下的環境(一個變數與兩個函式)就會封入閉包中與函式相依相存 所以 兩個 method 都有屬於自己的**獨立**閉包範圍 feints 無法被訪問,而且只能透過特定方法訪問/存取 就像是其他程式語言的私有變數一樣 ### 5.2.2 伴隨回呼來使用閉包 另一個常用閉包的地方是處理callback函式,通常在這類函式中需要存取外部的資料 * 程式列表 5.4 ```htmlmixed= <div id="box1">First Box</div> <!-- 要做出動畫效果的元素 --> <script> function animateIt(elementId) { // 宣告一個函式animateIt及參數elementId var elem = document.getElementById(elementId); // 宣告一個變數elem // 並將參數id是 elementId 的元素 賦值給他 var tick = 0; // 等等要用來決定元素位移距離的參數 var timer = setInterval(function(){ if (tick < 100) { elem.style.left = elem.style.top = tick + "px"; // 用 inline 渲染 目標元素的 style tick++; // 逐步增加tick的值,當tick不小於100時會停止setInterval } else { clearInterval(timer); // 取消timer的setInterval設置間隔時間的重複任務 assert(tick === 100, "Tick accessed via a closure."); // assert 略 assert(elem, "Element also accessed via a closure."); // assert elem是否有值 assert(timer, "Timer reference also obtained via a closure." ); // assert timer是否有值 } }, 10); } animateIt("box1"); // 將classname "box1" 傳入函式中 </script> ``` [code pen Nanja ver 2.0 ch.5 Listing 5.4](https://codepen.io/nikiweikame/pen/yLEJmaZ?editors=1000) 使用閉包儲存三個變數 elem、tick、timer,並利用閉包的特性將三個變數放進函式中,而不是在全域宣告三個參數,並作為函式的引數傳入函式中 elem、tick、timer在不同的函式呼叫中是完全獨立的個體,也就是說可以同時執行多個動畫效果,而不會乎相衝突 但如果是在全域宣告三個變數,當需要執行多組動畫效果時,會需要多組三個變數 * 我們可以利用閉包,一次執行多組事情 ## 5.3 使用執行背景空間(execution context)追蹤程式執行 使用函式常常會遇到,執行一個函式時,會在函式裡面呼叫另函式, 然後剛一個呼叫出的函式裏頭又呼叫了另一個函式 在經歷了眾多函式的相互呼叫之後JavaScript是如何確保函式執行的序列 一個函式結束之後要繼續執行哪一個函式 * 執行背景空間 ch.2 有提到 JavaScript程式有兩種主要類型 * 全域程式:放在所有函式之外,也就是獨立存放於全域之中 * 函式程式:被包含在函式裡面 而執行背景空間也有兩種類型 * 全域執行背景空間:JavaScript開始執行執行時建立的背景空間,只會有一個 * 函式執行背景空間:每次函式呼叫時都會建立一個背景空間 ch.2 JavaScript是單執行的序的執行模型,一次只能執行一段程式碼 所以一次會只有一個執行的背景空間在運作 當函式中呼叫到其他函式時,會暫停目前的執行背景空間,並**往上**建立新的執行背景空間堆疊在既有的執行背景空間上 當函式執行完成後,他的函式執行背景空間會被捨棄,**往下**繼續執行跟剛被暫停的函式 這種追蹤函式執行空間的方法被稱作**執行背景空間堆疊**或是**呼叫堆疊** * 程式列表 5.5 ```javascript= function skulk(ninja) { report(ninja + " skulking"); } // 建立一個 呼叫其他函式(report)的 函式 function report(message) { console.log(message); } // 建立一個 呼叫內建函式(console.log)的 函式 skulk("Kuma"); skulk("Yoshi"); // 於全域呼叫 剛剛建立的函式 ``` ![執行背景空間](https://i.imgur.com/VMcujiz.png) 1. 初始的在全域執行背景空間 2. 暫停全域執行背景空間,並在上面建立一個函式執行背景空間執行函式skulk 3. 暫停函式執行背景空間,並在上面建立一個函式執行背景空間執行函式report (console.log部分被省略了) 4. 執行完函式report,捨棄report函式執行背景空間,回到下面的skulk函式執行背景空間 5. 執行完函式skulk,捨棄skulk函式執行背景空間,回到下面的全域執行背景空間 執行背景空間是一個JavaScript的內部概念,可以在JavaScript除錯中使用他 ![除錯步驟](https://i.imgur.com/VPcv63t.png) ## 5.4 使用字彙環境來追蹤識別項 字彙環境 是JavaScript作用範圍界定機制 ,口語稱作範圍(Scope) 所有在當下環境宣告或是定義的變數名稱及對應的值都會儲存到當下的字彙環境中 當我們呼叫一個變數名稱的時候,JavaScrit會在當下的環境(字彙環境)去找尋這個變數名稱及代表的意義 ### 5.4.1 巢狀程式 (code nesting) 一個程式碼可以包含在另一段程式碼裡面 ![code Nest](https://i.imgur.com/1rH59mJ.png) 1. function skulk 被包在全域裡面 2. function report 被包在函式skulk裡面 3. for 迴圈 被包在函式report裡面 * 巢狀內部的程式碼可以讀取外部程式碼所定義的變數→5.4.2 ### 5.4.2 巢狀程式與字彙環境 先前說到 1. 字彙環境 可以儲存當下所有的變數名稱與所對應的值 2. 巢狀程式 是可以一個程式碼可以包含在另一段程式碼裡面 理論上 每個巢狀函式都有屬於自己的**字彙環境** 但是在特定的執行背景空間中,除了讀取當下的字彙環境外,還會逐一讀取外部的字彙環境所定義的所有變數 ![code nest and mapping](https://i.imgur.com/NQGYeeo.png) * 尋找 intro 檢查report環境→有 * 尋找 action 檢查report環境→沒有 檢查report外部環境 skulk 檢查skulk環境→有 * 尋找 ninja 檢查report環境→沒有 檢查report外部環境 skulk 檢查skulk環境→沒有 檢查skulk外部環境 global 檢查global環境→有 當建立一個函式或是宣告一的變數的時候,其名稱與其值會儲存在當下的字彙環境,更明確的說就是儲存在名為[[Environment]]的內部屬性中 巢狀程式就是以當下的[[Environment]]為基礎在上面建立一個新環境 ## 5.5 瞭解JavaScript的變數類型 * var * let * const ### 5.5.1 變數的可變性 * 可變的變數 * var * let * 不可變的變數 * const #### const 變數 建立的時候必須給一個初始值,之後就不能指派新的值給他(無法改變值) * 用途:指派一些不應該被重新指派新值的變數 * 優點: * 對程式碼保護、防止不必要的修改 * 優化JavaScript引擎 * 程式列表 5.6 ```javascript= const firstConst = "samurai"; assert(firstConst === "samurai", "firstConst is a samurai"); // 用const指派一個值給變數firstConst,並確認指派結果 try{ firstConst = "ninja"; // 在try...catch...statement中,對指派新的值給firstConst fail("Shouldn't be here"); // 如果上面指派順利通過會執行這一行 } catch(e){ pass("An exception has occured"); // 出現error會執行這行 } assert(firstConst === "samurai", "firstConst is still a samurai!"); // 並在一次確認指派的值沒有被改變 const secondConst = {}; // 指派一個物件給變數secondConst secondConst.weapon = "wakizashi"; // 為物件secondConst增加新的屬性 assert(secondConst.weapon === "wakizashi", "We can add new properties"); // 確認增加的屬性 // 透過const指派的變數,沒辦法透過指派修改其值,但是可以對其內容做修改 const thirdConst = []; assert(thirdConst.length === 0, "No items in our array"); thirdConst.push("Yoshi"); assert(thirdConst.length === 1, "The array has changed"); // 除了物件之外 陣列也適用剛剛的規則 ``` ### 5.5.2 用來定義變數的關鍵字彙環境 var let const 三個定義變數的方式依據它們語字彙環境的關係 以此做分類 var一類 let const 分另一類 #### 使用 var 定義變數 var 所宣告/定義的變數 會**無視區塊結構** 綁定在**最鄰近**的函式或是全域的字彙環境 也就是 他不會綁定在**程式區塊**的字彙環境上 * 程式列表 5.7 ```javascript= var globalNinja = "Yoshi"; // 在全域定義一個變數 function reportActivity(){ // 在全域宣告一個函式 var functionActivity = "jumping"; // 在函式中定義個等等迴圈要用的變數 for(var i = 1; i < 3; i++) { var forMessage = globalNinja + " " + functionActivity; // 迴圈中全定義了兩個變數 assert(forMessage === "Yoshi jumping", "Yoshi is jumping within the for block"); assert(i, "Current loop counter:" + i); // 在迴圈內 變數 forMessage 跟 i 都能 在迴圈中的字彙環境找到對應的值 } assert(i === 3 && forMessage === "Yoshi jumping", "Loop variables accessible outside of the loop"); } // 在迴圈外 變數 forMessage 跟 i 都能 在函式中的字彙環境找到對應的值 reportActivity(); assert(typeof functionActivity === "undefined" && typeof i === "undefined" && typeof forMessage === "undefined", "We cannot see function variables outside of a function"); // 在函式外(全域) 無法在全域的字彙環境找到對應的值 ``` 上面有三個字彙環境 * 全域 變數 globalNinja * 函式reportActivity 變數 functionActivity forMessage i * for迴圈 **區塊環境** 沒東西 因為var會無視區塊環境 所以 ES6新增了 let 跟 const #### 使用 let 與 const 於指定區塊範圍內指定變數 跟var相比 let 與 const 更加直覺 會把變數定義於最接近的字彙環境中(區塊環境、迴圈環境、函式環境或是全域環境) 用let 跟 const改寫 程式列表5.7 * 程式列表 5.8 ```javascript= const globalNinja = "Yoshi"; // 用const於全域環境定義變數 function reportActivity(){ const functionActivity = "jumping"; // 用const於函式環境定義變數 for(let i = 1; i < 3; i++) { let forMessage = globalNinja + " " + functionActivity; assert(forMessage === "Yoshi jumping", "Yoshi is jumping within the for block"); assert(i, "Current loop counter:" + i); // 用let於for迴圈 定義 兩個變數 // 在迴圈內 變數 forMessage 跟 i 都能 在迴圈中的字彙環境找到對應的值 } assert(typeof i === "undefined" && typeof forMessage === "undefined", "Loop variables not accessible outside the loop"); } // 在迴圈外 變數 forMessage 跟 i 都無法在函式中的字彙環境找到對應的值 reportActivity(); assert(typeof functionActivity === "undefined" && typeof i === "undefined" && typeof forMessage === "undefined", "We cannot see function variables outside of a function"); // 在函式外(全域) 無法在全域的字彙環境找到對應的值 ``` ### 5.5.3 在字彙環境中註冊識別項 hoisting的真相 程式碼是逐行執行的,但是還沒有被宣告的函式卻可以執行 #### 註冊識別項的過程 (JavaScript啟動時的執行順序) JavaScript在執行的時候會先讀取所有已宣告的變數及函式,之後才會進入執行階段(逐行執行程式碼內容) ![Imgur](https://i.imgur.com/oj0pAUG.png) 1. 確認是不是要建立一個函式環境 建立argument物件、函式的參數、隱性參數 2. 確認是不是要建立一個函式環境或是全域環境 將範圍內的所有**函式宣告**註冊進字彙環境中,如果名稱重複則覆寫 3. 確認是否是區域環境 * 是,將let const宣告的變數註冊進字彙環境 * 不是,將var宣告的變數註冊進字彙環境 var 註冊的值 初始為 undefined let const 則會出現初始化之前沒有使用權 ```javascript= for (let i = 0 ;i <= 0 ; i ++){ console.log(AA); let AA = 20; } // Uncaught ReferenceError: Cannot access 'AA' before initialization ``` #### 在函式宣告前呼叫他 * 程式列表 5.9 ```javascript= assert(typeof fun === "function", "fun is a function even though its definition isn’t reached yet!"); assert(typeof myFunExp === "undefined", "But we cannot access function expressions"); assert(typeof myLamda === "undefined", "Nor lambda functions"); // 檢驗下面三個變數的類別,僅有函式是被註冊識別項步驟2被建立 // var僅有名稱被建立 但是值被初始化為undefined function fun(){} var myFunExpr = function(){}; var myLambda = (x) => x; ``` #### 覆寫函式 * 程式列表 5.10 ```javascript= assert(typeof fun === "function", "We access the function"); // 透過註冊識別項的步驟2 函式先會被註冊於字彙環境中 var fun = 3; // 對同樣的變數賦予新的值(新的參照) assert(typeof fun === "number", "Now we access the number"); // 得到新的結果 function fun(){} assert(typeof fun == "number", "Still a number"); ``` 變數的提升hoisting hoisting 是註冊識別項的簡化觀點 實質上 變數 並沒有移動到任何地方 ## 5.6 探索閉包的運作方式 用**執行背景空間**與**字彙環境的概念**重新檢視閉包 ### 5.6.1 重新檢視如何用閉包來摹擬私有變數 1. 使用函式建構器 建立出新的物件及獨立的字彙環境 2. 函式利用巢狀程式與字彙環境追蹤識別項(變數) * 程式列表 5.11 ```javascript= function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } // 宣告一個建構器函式 var ninja1 = new Ninja(); // 用 new 建立新物件 // 用 建構器函式 在新物件中建立一個的字彙環境中 並追蹤所有變數(feints) // 用 建構器函式 在物件裡面建立兩個函式 // 這兩個透過建構器函式建立的函式 可以讀取建構器函式所建立的字彙環境 assert(ninja1.feints === undefined, "And the private data is inaccessible to us."); ninja1.feint(); // 方法feint 先在 自身的函式字彙環境 搜尋 識別項(變數) feints → 沒有 // 往外部 建構器函式 在新物件ninja1中建立一個的字彙環境中 尋找 → 有了 // 找到了 識別項(變數) feints ,放進函式內操作 assert(ninja1.getFeints() === 1, "We're able to access the internal feint count."); var ninja2 = new Ninja(); assert(ninja2.getFeints() === 0, "The second ninja object gets it’s own feints variable."); ``` * 程式列表 5.11 上 ```javascript= function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } // 宣告一個建構器函式 var ninja1 = new Ninja(); var ninja2 = new Ninja(); // 用 new 建立新物件 // 用 建構器函式 在新物件中建立一個的字彙環境中 並追蹤所有變數(feints) // 用 建構器函式 在物件裡面建立兩個函式 // 這兩個透過建構器函式建立的函式 可以讀取建構器函式所建立的字彙環境 ninja1.feint(); ``` ![Exploring-1](https://i.imgur.com/Y7WrH1L.png) * 程式列表 5.11 下 ```javascript= ninja1.feint(); // 方法feint 先在 自身的函式字彙環境 搜尋 識別項(變數) feints → 沒有 // 往外部 建構器函式 在新物件ninja1中建立一個的字彙環境中 尋找 → 有了 // 找到了 識別項(變數) feints ,放進函式內操作 assert(ninja1.getFeints() === 1, "We're able to access the internal feint count."); assert(ninja2.getFeints() === 0, "The second ninja object gets it’s own feints variable."); ``` ![Exploring-2](https://i.imgur.com/ri69bNP.png) * 只有透過建構式函式建立的method方法,才能讀取到建構器函式建立的字彙環境 ```javascript= function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } let shark = new Ninja(); shark.getFeints2 = function(){ return feints; }; shark.getFeints(); // 0 shark.getFeints2(); // Uncaught ReferenceError: feints is not defined ``` ### 5.6.2 留意私有變數 * 將可以讀取私有變數的method指派給其他物件的method,一樣可以讀取私有變數 * 程式列表 5.12 ```javascript= function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } var ninja1 = new Ninja(); ninja1.feint(); var imposter = {}; imposter.getFeints = ninja1.getFeints; // 建立一個空物件,並將ninja1可以讀取私有屬性的method指派給imposter.getFeints assert(imposter.getFeints () === 1, "The imposter has access to the feints variable!"); // 可以透過其他物件呼叫到ninja1的私有屬性feints ``` * 私有變數的存取/呼叫與函式緊緊相依,跟物件沒有關係 ```javascript= ninja1 = {} assert(typeof ninja1.getFeints === "undefined", "The imposter has access to the feints variable!"); assert(imposter.getFeints () === 1, "The imposter has access to the feints variable!"); ``` * 即使物件消失,函式還在,就可以使用閉包中的變數 ### 5.6.3 重新檢視閉包和回呼範例 用字彙環境解釋 回呼範例為何不會衝突 * 程式列表 5.13 ```htmlmixed= <div id="box1">First Box</div> <div id="box2">Second Box</div> <script> function animateIt(elementId) { // 被呼叫的時候 會建立 新 的字彙環境 並追蹤所有變數 elem tick timer var elem = document.getElementById(elementId); var tick = 0; var timer = setInterval(function(){ // 每呼叫一次回呼函式都會建立新的字彙環境 並找尋使用的變數 // 回呼函式的字彙環境內找不到 往外部的字彙環境尋找 // 在外部的 函式的animateIt 呼叫時新建立的 找到 找尋使用的變數 // 由於 函式的animateIt的字彙環境是被呼叫的時候新建立的 // 所以 不同的呼叫會有不同得字彙環境 彼此之間不會相互衝突 if (tick < 100) { elem.style.left = elem.style.top = tick + "px"; tick++; } else { clearInterval(timer); assert(tick === 100, "Tick accessed via a closure."); assert(elem, "Element also accessed via a closure."); assert(timer, "Timer reference also obtained via a closure." ); } }, 10); } animateIt("box1"); animateIt("box2"); </script> ``` ## 5.7 總結 * 閉包的範圍 宣告函式時所有於作用範圍中的所有變數 * 閉包的運用 * 模擬私有變數 * 使用回呼時可以簡化程式碼 * 執行背景空間的堆疊 * 字彙環境(作用範圍)追蹤識別項(變數) * 定義全域範圍、函式範圍、區塊範圍的變數,近一步理解hoisting的原理 * 宣告/定義變數所使用的三個關鍵字var、let、const * var 忽略區塊環境,將變數綁定在最鄰近的全域環境或是函式環境中 * let const 在最鄰近的環境(包含區塊)定義變數,其中const只能被指派一次 * 即使物件消失,函式還在,依舊可以使用閉包中的變數 ## 5.8 習題 1. 閉包允許函式: a. 存取在定義函式時範圍內的外部變數 ← 5.1 P.104 b. 存取函式被呼叫時範圍內的外部變數 2. 閉包會帶來: a. 程式碼數量上的開銷 b. 記憶體的開銷 ← 5.1 P.107 c. 處理效能上的開銷 3. 在以下程式碼範例中,標記出藉由閉包來存取的識別項: mark the identifiers accessed through closures: 標示出使用閉包的識別項 ```javascript= function Samurai(name) { var weapon = "katana"; this.getWeapon = function(){ return weapon; }; this.getName = function(){ return name; } this.message = name + " wielding a " + weapon; this.getMessage = function(){ return this.message; } } var samurai = new Samurai("Hattori"); samurai.getWeapon(); samurai.getName(); samurai.getMessage(); ``` 會使用閉包的是函式 透過建構器函式建立的函式即是方法 當函式的字彙環境建立的時候 註冊在函式的識別項有三個 1. 函式的Argument物件及參數 2. 宣告的函式名稱 3. 透過var宣告的變數名稱 ```javascript= function Samurai(name) { var weapon = "katana"; ← this.getWeapon = function(){ // 使用透過var宣告的變數名稱(識別項) return weapon; }; this.getName = function(){ // 使用函式的參數(識別項) return name; } this.message = name + " wielding a " + weapon; // 這不是函式 this.getMessage = function(){ // 使用物件的屬性 return this.message; } } ``` 4. 在下面的程式碼中,建立了多少個執行背景空間?以及執行背景空間堆疊的大小最大是多少? ```javascript= function perform(ninja) { sneak(ninja); infiltrate(ninja); } function sneak(ninja) { return ninja + " skulking"; } function infiltrate(ninja) { return ninja + " infiltrating"; } perfom("Kuma") ``` 1. 一個函式被呼叫時,會建立相對應的函式背景空間 上面有三個函式被呼叫perfom、sneak及infiltrate所以有三個函式執行背景空間 再加上全域背景執行空間,合計**四個執行背景空間** 2. 堆疊三層,如下表 | sneak | infiltrate | |:-------:|:----------:| | perform | perform | | 全域 | 全域 | --- 5. JavaScript中哪個關鍵字允許我們定義不能重新被指派新值得變數 不能重新被指派新值得變數 → const 6. var 與 let 有什麼區別? | 類別 | var | let | |:-----------:|:-------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------:| | 定義的 位置 | 鄰近的全域或是函式的字彙環境 | 鄰近在最接近的的字彙環境 (全域、函式或區塊) | | 註冊 順序 | 在全域環境或函式環境下,會在函式宣告之後,註冊於當下的字彙環境,並賦予undefined的參照 | 僅在區塊環境中會優先註冊於當下 環境,但是會形成暫時性死區(Temporal Dead Zone,TDZ) | | 重複 宣告 | 可 | 否 | 7. 下面的程式碼哪裡會拋出異常?為什麼? ```javascript= getNinja(); getSamurai(); function getNinja() { return "Yoshi"; } var getSamurai = () => "Hattori"; ``` 在建構字彙環境的時候 1. 判斷這是不是一個函式環境 → 不是,這是一個全域環境 2. 判斷這是不是函式環境或是全域環境 → 是,這是一個全域環境 * 將宣告的函式註冊到環境的識別向上 → getNinja 作為函式註冊到識別向上 3. 判斷這是不是一個區塊環境 → 不是,這是一個全域環境 * 將var選告的變數名稱註冊到識別項上,並給予參照為undefined 現在我們的環境有 1. 函式getNinja 2. 變數getSamurai,其值為undefined 執行到getNinja() 執行環境有相對應的函式的函式所以pass 執行到getSamurai() 執行環境有相對應的變數getSamurai,但是他現在還不是函式,所以會出現錯誤,getSamurai不是一個函式。 Uncaught TypeError: getSamurai is not a function