Lin
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 第五章 大師級函式:閉包與範圍 ###### tags: `好想工作室`、`忍者讀書會` ## 5.1 瞭解閉包 #### ▌Scope(範圍、範疇、作用域) **Scope就是變數可以被看見、被使用的範圍。** ![](https://i.imgur.com/NYaWr5W.jpg) * **Global Scope** 全域作用域,變數不在function或block裡面,而是在全域中宣告,因此任何地方都能使用該變數。 ```javascript! var totalEggs = 6 function collectEggs(){ console.log(totalEggs) //6 } collectEggs() ``` * **Local Scope** 又分為兩種區域作用域:Function Scope、Block Scope(ES6) * **Function Scope** function有自己的作用域,所以在function裡面宣告變數,在function之外的地方都沒辦法使用。 ```javascript! function collectEggs(){ var totalEggs = 6 } console.log(totalEggs) //totalEggs is not defined ``` 即便先執行了這個function,外面仍然無法取得function內部宣告的變數。 ```javascript! function collectEggs(){ var totalEggs = 6 } collectEggs() console.log(totalEggs) //totalEggs is not defined ``` 這是因為function執行時會進入call stack,這裡的變數totalEggs會存到stack memory中(stack memory是用來保存由函式所產生的暫時性變數,或稱區域變數),stack memory使用的是暫存性的記憶體空間,所以當function執行結束之後,這些記憶體就會跟著消失。 (如下圖流程所示) ![](https://i.imgur.com/HZWAVEK.jpg) ![](https://i.imgur.com/4BBKABQ.jpg) ![](https://i.imgur.com/GBPza2H.jpg) ![](https://i.imgur.com/TuHxpIC.jpg) * **Block Scope(ES6)** ES6之前,只有function可以建立scope。 ES6之後,出現了let、const宣告,使得我們可以用「大括號」建立scope。(大括號範圍就是block,例如if判斷式、while或for迴圈) #### ▌程式列表5.1 一個簡單的閉包 ```javascript! var outerValue = "ninja"; function outerFunction() { assert(outerValue === "ninja","I can see the ninja."); } outerFunction(); ``` 如果用上面scope的概念來看,會覺得`outerValue === "ninja"`為true,這是再正常不過的事情,因為`outerValue`是在全域中宣告,所以任何地方都可以使用到這個變數。 但事實上`outerFunction()`能看到、使用自己function scope之外的變數,其實就是在建立一個閉包。 我們在看下個複雜點的例子。 #### ▌程式列表5.2 另一個閉包的例子 ```javascript! var outerValue = "samurai"; var later; function outerFunction() { var innerValue = "ninja"; function innerFunction() { assert(outerValue === "samurai", "I can see the samurai."); assert(innerValue === "ninja", "I can see the ninja."); } later = innerFunction; } outerFunction(); later(); ``` **(我們所想的)** 就像前面提到的,當執行一個function,變數若是在function scope裡面建立的,會被存放到stack memory,一個暫存性的記憶體空間。 所以說當`outerfunction()`一執行結束,照理說`innerValue = "ninja"`也會被消失才對,因此我們會預期第二個檢查失敗。 ![](https://i.imgur.com/1AU52rK.jpg) 不過...... ![](https://i.imgur.com/t5SFHPu.png) **(事實上)** 我們執行`innerFunction()`的時候,還是可以抓到`innerValue`這個變數。所以到底是什麼原因讓`innerValue`這個變數仍然是活的?答案就是閉包。 當我們在`outerfunction()`裡宣告`innerFunction()`時,做了兩件事情: 1. 定義了一個函式宣告`innerFunction()` 2. 建立了一個閉包: * 閉包包含了函式定義`innerFunction()`以及在建立函式時存在於作用範圍內的所有參數。 * 如下圖,閉包就像一個保護用的氣泡,只要函式仍然存在,`innerFunction()`的閉包就會讓函式範圍內的變數一直保持在有效狀態下。 ![](https://i.imgur.com/CHabfzV.jpg) 如果用程式碼來看他們之間的關係,就會如下圖。因此如果要用一句話來形容什麼是閉包?可以套用[techsith教學影片](https://youtu.be/71AtaJpJHw0?t=705)的一段話來形容:"Closures are nothing but FUNCTIONS WITH PRESERVED DATA",閉包就是函式包含該函式所保留的資料。 可以想像就是,當一個function使用到了自己function scope之外的變數,這個function包含使用到了的變數,就會形成一個閉包。 ![](https://i.imgur.com/9ic0mem.jpg) 那這裡再稍微補充一下! **(以下觀念來自[胡立的文章](https://blog.huli.tw/2018/12/08/javascript-closure/))** 上面提到「function使用到了自己function scope之外的變數」,其實這樣的變數有一個特殊的名字。 對於`innerFunction()`這個function來說,`innerValue`或者`outerValue`都不是它自己的變數,而這種不在自己作用域中,也不是被當成參數傳進來的變數,就可以被稱作「free variable(自由變數)」。 所以對`innerFunction()`來說,`innerValue`以及`outerValue`都是自由變數。 另外,一個function在自己的作用域中找不到變數,就會往外面一層的作用域尋找,如果還是找不到,就會再往上一層直到找到為止(如果到全域還是找不到就會拋出錯誤),這個過程就會構成一個「Scope Chain(作用域鏈)」。 **(以下觀念來自[Fireship的影片](https://www.youtube.com/watch?v=vKJpN5FAeF4&list=PLJ1djWumFFcno2X_dXvfjupsdpHxzc55k&index=2&t=122s))** 上面有提到說「閉包就會讓函式範圍內的變數一直保持在有效狀態下」,這是因為當closure建立,會將那些包含在閉包內的變數,給存放到heap memory。 ![](https://i.imgur.com/x7uJJGa.jpg) 與stack memory不同,stack memory是function執行結束就會把記憶體空間給釋放,而heap memory則是function執行結束後,裏頭存放的東西仍然存在,直到我們去做清除的動作。(至於如何清除,不同語言有不同的方法,詳情可見[Stack vs. Heap](https://medium.com/joe-tsai/stack-vs-heap-b4bd500667cd)) 所以說closure的缺點就在這邊,它會相對地比較佔記憶體空間。 **** ## 5.2 開始使用閉包 在5.2章節中,我們會來看看如何在JavaScript中使用閉包。 (分別為模擬私有變數、處理回呼) ### 5.2.1 模擬私有變數 JavaScript本身不支援私有變數,但可以藉由閉包來做出類似的功能。 #### ▌程式列表5.3 使用閉包來模擬私有變數 ```javascript! function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } var ninja1 = new Ninja(); ninja1.feint(); assert(ninja1.feints === undefined, "And the private data is inaccessible to us."); 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."); ``` **分析** 這裡建立了一個建構器函式:Ninja。 ```javascript! function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } ``` * `feints` 變數:,用來紀錄狀態。 * `getFeints` 方法:因為變數`feints`在`Ninja()`function裡面被宣告,所以只能在這個constructor function裡面被使用、存取, `getFeints` 方法便是為了允許範圍之外的程式碼可以存取到這個變數的值,而有了`getFeints`讀值器的方法。 * `feint` 方法:用來控制變數的值,在本例中,此方法可以增加`feints`的值。 接著我們便可以新建立ninja1物件,並且在此物件上呼叫`feint`方法,呼叫`feint`方法時,它會增加ninja1的`feints`值。 ```javascript! var ninja1 = new Ninja(); ninja1.feint(); assert(ninja1.feints === undefined, "And the private data is inaccessible to us."); assert(ninja1.getFeints() === 1, "We're able to access the internal feint count."); ``` 接著從測試中的結果可知: 1. `ninja1.feints === undefined`為true,代表我們無法從外部存取函式內部的變數。 2. `ninja1.getFeints() === 1`為true,代表雖然我們無法從外部取值,但還是有辦法更改`feints`這個私有變數的值。 而最後,我們再使用Ninja建構器建立一個ninja2物件。 ```javascript! var ninja2 = new Ninja(); assert(ninja2.getFeints() === 0, "The second ninja object gets it’s own feints variable."); ``` 可以從測試中得知新建立的ninja2物件,它有屬於自己的feints變數。 **結論** 利用閉包可以讓忍者的狀態被保持在一個方法中,而無法被使用者直接存取。因為閉包讓內部方法可以存取變數,但是位於建構器函式外部的程式碼則不行。 這個方法讓`feints`變數,就像是一個真正的私有變數一樣。 ![](https://i.imgur.com/FBbDybx.jpg) ### 5.2.2 伴隨回呼來使用閉包 閉包的另一個常見用途式處理回呼,也就是函式在稍後的某個時間點被呼叫。通常在這些函式裡,我們需要時常存取外部資料。 #### ▌Callback function 回呼函式 回呼函式就是把B函式當作A函式的參數,透過A函式來呼叫B函式,而這個被當作參數帶入的B函式將在「未來的某個時間點」被呼叫與執行(是一種非同步事件的一種方式) 舉例來說,要進入霍格華茲的巫師「被叫到名字」後要「上前戴上分類帽」,「戴上分類帽」後就會被「分學院」。 ![](https://i.imgur.com/aeFuSqH.jpg) 如果拿程式碼來說明就會像下面那樣: ```javascript= wizard.addEventListener("被叫到名字" ,function(){ 戴上分類帽; wizard.addEventListener("戴上分類帽" ,function(){ 分學院; }) }) ``` 我們對巫師做事件監聽,當巫師被叫到名字後,才會執行事件監聽第二個參數中的function。所以`戴上分類帽`這個動作只會在滿足了`被叫到名字`這個條件才會被動地去執行,我們就可以說這是一個 Callback function。 Callback function常見例子有setTimeout()、setInterval()、DOM 的事件監聽、從資料庫或遠端伺服器請求資料等等。 接著忍者裡面的範例會使用setTimeout()來做說明。 #### ▌程式列表5.4 在回呼計時器裡使用閉包 ```javascript! function animateIt(elementId) { var elem = document.getElementById(elementId); var tick = 0; var timer = setInterval(function(){ if (tick < 100) { elem.style.left = elem.style.top = tick + "px"; tick++; } else { clearInterval(timer); } }, 10); } animateIt("box1"); ``` 上面範例利用了closure的方式達到製作動畫的效果,它使用了一個匿名函式做為setInterval的參數,來完成目標div元素的動畫效果。這個匿名函式藉由閉包來存取三個變數:elem、tick、timer。 * elem:ID為"box1"的DOM元素參照 * tick:片刻計數器 * timer:計時器setInterval的參照 接著,來修改一下上述範例: * [第一個codepen範例](https://codepen.io/fgfjgror/pen/GRGjvyM?editors=1011): 現在如果將這三個變數都移出`animateIt()`函式外面,讓這三個變數變成全域變數,動畫仍然可以順利執行的。 代表說我們可以不必利用closure,就能做到一模一樣的效果,所以為什麼還要建立閉包?這就要看到第二個範例了。 * [第二個codepen範例](https://codepen.io/fgfjgror/pen/poKErBX) 如果我們將變數儲存在全域範圍中,我們每增加一個動畫,就要為該動畫設置三個變數,因此如果我們藉著函式內部定義變數,在計時器進行回呼時透過閉包讓那些變數可以被使用,每當我們呼叫`animateIt()`,每個動畫都可以獲得私有變數「氣泡」。 ![](https://i.imgur.com/DwQy2QV.jpg) ![](https://i.imgur.com/wIq7DEe.jpg) **** ## 5.3 使用執行背景空間追蹤程式執行 * 在JS中,函式是最基本的執行單元 * JS程式: 1. **全域程式**:放置在所有函式之外 2. **函式程式**:被包含在函式之中 ![](https://i.imgur.com/oHgcVVf.jpg) * 當我們的程式被JS引擎執行的時候,每個敘述句都在一個特定的++執行背景空間++中執行。 * ++執行背景空間++: 1. **全域執行背景空間**:只會有一個全域執行背景空間,它是在我們的JS程式開始執行時建立的。 2. **函式執行背景空間**:每次呼叫函式就會建立一個新的函式執行背景。 :::warning * 注意:函式背景空間 vs. 函式執行背景空間 | 名稱 | 解釋 | | ---------------- | ------------------------------------------------------ | | 函式背景空間 | 函式被呼叫時所屬的物件,可以使用this關鍵字來存取它 | | 函式執行背景空間 | 這是JS引擎所使用的一種內部概念,並用它來追蹤函式的執行 | ::: * JS是單執行緒的執行模型(Single Threaded),也就是說他一次只能執行一段程式碼 * 每當一個函式被呼叫時,都必須停止當前的執行背景空間,然後再建立一個新的函式執行背景空間,當函式的任務完成後,它的函式執行背景空間通常就會被丟棄,並恢復到原本呼叫者的執行背景空間。 ---> 所有的執行背景空間都需要進行追蹤(無論執行中、等待中) ---> 利用堆疊(stack)追蹤,它被稱為執行背景空間堆疊(或稱呼叫堆疊,也就是在筆記5.1提到的call stack) 直接來看看程式碼範例。 #### ▌程式列表5.5 建立執行背景空間 ```javascript! function skulk(ninja) { report(ninja + " skulking"); } function report(message) { console.log(message); } skulk("Kuma"); skulk("Yoshi"); ``` 上述範例在執行背景空間的堆疊行為如下圖,但是用看的,不如用chrome的dev tool來看看它實際的行為。(這裡直接用dev tool去操作程式列表5.5) ![](https://i.imgur.com/c3BhMUc.jpg) **** ## 5.4 使用字彙環境來追蹤識別項 * 字彙環境(Lexical Scope):JS引擎的一個內部結構,用於追蹤從++識別項++到特定變數的對應(mapping) * ++識別項++(identifier):(參考[Chidre'sTechTutorials影片](https://www.youtube.com/watch?v=UzKMBLWeQ-A)) 舉例來說:變數的名稱、常數的名稱、陣列的名稱、函式的名稱、物件的名稱等等,都可以叫做識別項。 :::warning * 注意: 字彙環境是JS作用範圍界定機制的內部實作,而人們經常會在口語上將他們稱為Scope(範圍、範疇、作用域) ::: * 字彙環境(或者就叫它scope)與JS程式碼的特定結構相關聯,這些特定結構包含:一個函式、一段程式區塊、catch區塊,他們都可以有個別的識別項對應。 :::warning * 注意: * ES6前:字彙環境只可以與函式相關聯(所以只有function scope) * ES6後:有了block scope的存在 ::: ### 5.4.1 巢狀程式(code nesting) * 字彙環境主要基於巢狀程式,也就是一個程式碼結構能夠被包含另一個程式碼結構中(如下圖) ![](https://i.imgur.com/QqxDNfa.jpg) * 每一次程式碼被執行時,每一個程式碼結構都獲得相關的字彙環境。 * 字彙環境的規則(或稱scope的規則): 「外層 Scope 無法取用內層變數,但內層 Scope 可以取用外層變數」 接著就要來看JS引擎是如何追蹤這些變數?我們又可以從哪裡存去這些變數?這都是透過字彙環境所達成的。 ### 5.4.2 巢狀程式與字彙環境 ![](https://i.imgur.com/Y6YLMwp.jpg) * report函式由skulk函式呼叫 * skulk函式由全域環境呼叫 每個執行背景空間具有與其相關聯的字彙環境,它包含了在該背景空間中所定義的所有識別項對應,例如: * 全域環境:保留了識別項ninja、skulk的對應 * skulk環境:保留了識別項action、report的對應 * report環境:保留了識別項intro的對應 ![](https://i.imgur.com/Uo68i1Y.jpg) 執行report函式時,JS引擎解析識別項的步驟: * 尋找intro: 1. 檢查report環境 -> <font color="green">有</font> * 尋找action: 1. 檢查report環境 -> <font color="red">無</font> 2. 檢查report的外部環境:skulk 檢查skulk環境 -> <font color="green">有</font> * 尋找ninja: 1. 檢查report環境 -> <font color="red">無</font> 2. 檢查report的外部環境:skulk 檢查skulk環境 -> <font color="red">無</font> 4. 檢查skulk的外部環境:global 檢查global環境 -> <font color="red">無</font> 除了存取對應的字彙環境中定義的識別項之外(例如report環境保留了識別項intro的對應),程式也常常存取在外部環境中定義的其他變數(例如report函式裡,存取了skulk的變數action、全域變數nunja) ---> 為了做到這一點,我們必須以某種方式來追蹤這些外部環境,而JS作法則是讓函式做為頭等物件。 每當建立一個函式時,指向該函式所屬字彙環境的參照,會儲存在名為[[Environment]]的內部屬性裡面(表示不能被直接存取或是操作)。 * skulk函式保持對全域環境的參照 * report函式保持對skulk環境的參照 ![](https://i.imgur.com/CFHlQtJ.jpg) **** ## 5.5 瞭解JavaScript的變數類型 * 在JS中,有三種關鍵字可用來定義變數: * var * let * const * 他們在兩個方面有所不同: * 可變性:【const】 vs. 【var、let】 * 字彙環境的關係:【var】 vs. 【const、let】 ### 5.5.1 變數可變性 * 如果用可變性來區分變數宣告的關鍵字,我們會這樣分成兩組:【const】 vs. 【var、let】 * 【const】:所有使用const定義的變數都是不可變的,意味著他們的值只能設置一次 * 【var、let】:典型的普通變數,其值可以根據需求改變許多次 這一個章節主要會來瞭解const變數的運作方式以及行為。 #### ▌const變數 * const變數類似於一般變數,但是它在宣告時就需要提供一個初始值,而在那之後,我們就不能指派新的值給它了。 * const變數常用於兩種目的: * 指定一些不應重新指派的變數(在本書中使用const,多半是這個目的) * 參照到一個固定值 例如,用名稱來表示一個浪人團最多可以由幾位浪人組成:MAX_RONIN_count #### ▌程式列表5.6 const變數的行為 ```javascript! const firstConst = "samurai"; assert(firstConst === "samurai", "firstConst is a samurai"); try { firstConst = "ninja"; fail("Shouldn't be here"); } catch (e) { pass("An exception has occured"); } assert(firstConst === "samurai", "firstConst is still a samurai!"); const secondConst = {}; secondConst.weapon = "wakizashi"; assert(secondConst.weapon === "wakizashi", "We can add new properties"); const thirdConst = []; assert(thirdConst.length === 0, "No items in our array"); thirdConst.push("Yoshi"); assert(thirdConst.length === 1, "The array has changed"); ``` **分析** 1-1. 先來定義一個const變數,並確認有指派一個值給它 ```javascript! const firstConst = "samurai"; assert(firstConst === "samurai", "firstConst is a samurai"); ``` 1-2. 若企圖指派一個新值給const變數,就會發生異常 ```javascript! try { firstConst = "ninja"; fail("Shouldn't be here"); } catch (e) { pass("An exception has occured"); } ``` / 2-1. 建立一個新的constant變數,並指派一個物件給它 ```javascript! const secondConst = {}; ``` 2-2. 我們無法指派一個全新的物件給secondConst變數,但可以修改它 ```javascript! secondConst.weapon = "wakizashi"; assert(secondConst.weapon === "wakizashi", "We can add new properties"); ``` 若在此處指派一個全新的物件給secondConst變數(如下方所示) ```javascript! const secondConst = {}; secondConst = { weapon: "wakizashi" } ``` 將得到以下錯誤訊息 ![](https://i.imgur.com/c583d47.png) / 3-1. 上述規則也適用於陣列。先建立一個新的constant變數,並指派一個陣列給它,且測試確認thirdConst為一個空陣列。 ```javascript! const thirdConst = []; assert(thirdConst.length === 0, "No items in our array"); ``` 3-2. 一樣無法指派全新的陣列給thirdConst變數,但可以修改它 ```javascript! thirdConst.push("Yoshi"); assert(thirdConst.length === 1, "The array has changed"); ``` **結論** 1. const變數的值只能在初始化時設置,不能在之後指派一個全新的值給它 2. 可以修改現有的值,只是不能完全複寫它 ### 5.5.2 用來定義變數的關鍵字和字彙環境 * 如果用根據他們與字彙環境的關係(i.e.他們的scope)來區分變數宣告的關鍵字,我們會這樣分成兩組:【var】 vs. 【const、let】 #### ▌使用var關鍵字 使用var關鍵字時,變數是定義在最鄰近的函式或全域字彙環境中(程式區塊block scope會被忽略!畢竟它是ES6後才出現的,比var還晚出生) #### ▌程式列表5.7 使用var關鍵字 ```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); } assert(i === 3 && forMessage === "Yoshi jumping", "Loop variables accessible outside of the loop"); } reportActivity(); assert(typeof functionActivity === "undefined" && typeof i === "undefined" && typeof forMessage === "undefined", "We cannot see function variables outside of a function"); ``` **分析** 1-1. 使用var來定義一個全域變數globalNinja為Yoshi ```javascript! var globalNinja = "Yoshi"; ``` 2-1. 在函式內使用var定義一個區域變數functionActivity為jumping 2-2. 在for loop裡使用var來定義兩個變數i、forMessage 2-3. 在for loop中測試我們可以存取到外面的區塊變數、函式變數、全域變數 2-4. 但在for loop外,也可以存取到for loop內的變數 ![](https://i.imgur.com/Vs0EsYt.jpg) 3-1. 在函式之外無法存取函式裡面的變數 ```javascript! assert(typeof functionActivity === "undefined" && typeof i === "undefined" && typeof forMessage === "undefined", "We cannot see function variables outside of a function"); ``` ![](https://i.imgur.com/Ce82BWm.jpg) 而上述程式碼中,有一段是JS的奇怪之處,就是在2-4測試的部分。我們竟然可以在區塊之外,繼續存取區塊內所定義的變數。 ---> 這是因為使用關鍵字var宣告的變數,總是會「註冊在最鄰近的函式或全域環境中」,而無視程式區塊的結構。 ![](https://i.imgur.com/qbPM4jr.jpg) block scope被忽視後,var定義的變數就會去找「最鄰近的函式或全域環境」,以上述程式碼為例,即reportActivity環境中(因為它是最鄰近的函式環境) ![](https://i.imgur.com/fcozIPe.jpg) 從上圖可見這裡擁有三個字彙環境: 1. 全域環境:註冊globalNinja變數的環境(因為這是最鄰近的函式或是全域字彙環境) 2. reportActivity環境: * 呼叫reportActivity函式時建立的 * 環境中包含的變數:(因為這是他們最鄰近的函式) * functionActivity * i * forMessage 3. for區塊:它是空的(因為用var定義的變數會忽略程式區塊) 由於這種行為非常奇怪,所以在ES6版本的JS提供了兩個新的變數宣告關鍵字。 #### ▌使用let和const來指定區塊範圍內的變數 使用let和const關鍵字,他們會把變數定義在最接近的字彙環境中(可以是區塊環境、迴圈環境、函式環境、全域環境)。 #### ▌程式列表5.8 使用關鍵字let和const ```javascript! const globalNinja = "Yoshi"; function reportActivity(){ const functionActivity = "jumping"; 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); } assert(typeof i === "undefined" && typeof forMessage === "undefined", "Loop variables not accessible outside the loop"); } reportActivity(); assert(typeof functionActivity === "undefined" && typeof i === "undefined" && typeof forMessage === "undefined", "We cannot see function variables outside of a function"); ``` ![](https://i.imgur.com/YbWoyyV.jpg) 從上圖可見這裡擁有三個字彙環境: 1. 全域環境:globalNinja變數定義在全域環境中 3. reportActivity環境:functionActivity變數定義在reportActivity環境中 4. for區塊:i變數、forMessage變數定義在for區塊中 這是因為使用let和const關鍵字,變數會被定義在最鄰近的環境中。 ### 5.5.3 在字彙環境中註冊識別項 首先我們知道JS是以滿直接的方式逐行執行的,再來看看以下例子: ```javascript! const firstRonin = "Kiyokawa"; check(firstRonin); function check(ronin) { assert(ronin === "Kiyokawa", "The ronin was checked!"); } ``` 如果說程式碼是逐行執行,那還沒執行到定義函式的地方,能夠呼叫到check函式嗎? ![](https://i.imgur.com/jE77Yt6.png) 測試結果還是可以的!不過程式碼是逐行執行的,JS引擎是怎麼知道有一個check函式的存在? #### ▌註冊識別項的過程 這是因為JS引擎使用了一點小手段,JS程式碼的執行其實分成了兩個階段: ![](https://i.imgur.com/UV25Xxv.jpg) 其確切行為取決於變數類型、環境類型: * 變數類型:let、var、函式宣告 * 環境類型:全域、函式、區塊 識別項在不同環境下的註冊過程:(JS程式碼的執行第一階段) ![](https://i.imgur.com/Sgxo0oO.jpg) 1. 第一步驟:如果我們建立一個函式環境,伴隨著的「函式參數」、「其引數值的隱含式arguments識別項」,會被建立出來。 2. 第二步驟:如果我們建立的式一個全域環境或是函式環境,會先掃描目前的程式碼,找出所有的「函式宣告」(不包含函式表達式、箭頭函式),針對發現的每個函式宣告,都建立一個新的函式,並將它綁定到該環境中的同名識別項上。 3. 第三步驟:找出「變數宣告」。 #### ▌在函式宣告前呼叫它 讓JS如此好用的一項特色,是函式定義的順序無關緊要。 #### ▌程式列表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"); function fun(){} var myFunExpr = function(){}; var myLambda = (x) => x; ``` 先別管測試,用簡單一點的方式來看上面的程式碼 ```javascript! typeof fun //function typeof myFunExp //undefined typeof myLamda //undefined function fun(){} //在執行JS程式碼之前,就會先註冊已宣告的函式 var myFunExpr = function(){}; //myFunExpr指向一個函式表達式 var myLambda = (x) => x; //myLambda指向一個箭頭函式 ``` * fun函式在執行JS程式碼之前,就已經先註冊了(已存在),因此能夠在不同地方呼叫它 * 函式表達式、箭頭函式不在此過程中,他們是在程式執行到其定義位置時才建立的,所以無法在他們建立之前存取這兩種函式。 #### ▌複寫函式 函式識別項可能會被複寫。 #### ▌程式列表5.10 複寫函式識別項 ```javascript! assert(typeof fun === "function", "We access the function"); var fun = 3; assert(typeof fun === "number", "Now we access the number"); function fun(){} assert(typeof fun == "number", "Still a number"); ``` 一樣先不管測試,比較方便觀察 ```javascript! typeof fun //function var fun = 3 //執行JS之前即註冊(註冊識別項的第三步驟:處理變數宣告) typeof fun //number function fun(){} //執行JS之前即註冊(註冊識別項的第二步驟:處理函式宣告) typeof fun //number ``` * 在此範例中,變數宣告和函式宣告都具有相同的名稱:fun * JS程式碼執行前,發生兩件事情: 1. `function fun(){}`:在註冊識別項的第二步驟,識別項fun以函式宣告被註冊。 2. `var fun = 3`:在註冊識別項的第三步驟,開始處理變數宣告,此時它會將數字3指派給識別項fun,這讓我們的識別項fun失去了對函式的參照,現在識別項fun變成了一個數字。 以上如果用簡化的觀點來看,就是常看到的一個術語「提升」(hoisting),但在技術上來說,變數和函式宣告並不會「移動」到任何地方,他們是在任何程式碼執行之前,在字彙環境中就先被取得並進行註冊。 **** ## 5.6 探索閉包的運作方式 ### 5.6.1 重新檢視如何使用閉包來模擬私有變數 #### ▌程式列表5.11 使用閉包來產生近乎私有的變數 ```javascript! function Ninja() { var feints = 0; this.getFeints = function(){ return feints; }; this.feint = function(){ feints++; }; } var ninja1 = new Ninja(); ninja1.feint(); var ninja2 = new Ninja(); ``` 回顧一下5.2章節,`feint`函式要存取`feints`變數,因此這裡就產生了閉包 ![](https://i.imgur.com/FBbDybx.jpg) 再來看看整體的部分,建構器函式是使用關鍵字new來呼叫的函式,因此每當我們呼叫一個建構器函式時,就等於建立了一個新的字彙環境,它會負責追蹤建構器函式的區域變數(以上述程式碼為例,即追蹤`feints`變數的新Ninja環境會被建立出來) 另外我們建立了兩個新函式:`getFeints`、`feint`,他們都具有對Ninja環境的參照。 ![](https://i.imgur.com/0FOu7X3.jpg) 當我們建立另一個Ninja物件:ninja2時,整個過程會再重複一遍。使用Ninja建構器所建立的每個物件都會得到屬於自己的方法(ninja1.getFeints()方法不同於ninja2.getFeints()方法) 他們把呼叫建構器函式時所定義的變數封閉起來,這些「私有」變數只能透過建構器函式中建立的物件方法(`getFeints`、`feint`方法)進行存取,而不能直接存取! ![](https://i.imgur.com/BKl0tOx.jpg) 接著看看呼叫ninja2.getFeints()時到底發生哪些事? 1. 當呼叫函式時,會建立一個新的執行背景空間。所以這裡建立了一個getFeints執行背景空間,並將其推送到執行堆疊。 ![](https://i.imgur.com/0M6OHhy.jpg) 2. 同時也建立了一個getFeints字彙環境,用來追蹤在此函式中定義的變數。 ![](https://i.imgur.com/sfAQphU.jpg) 3. getFeints字彙環境會取得建立getFeints函式時所屬的環境,也就是建立ninja2物件時有效的Ninja環境作為其外部環境。 ![](https://i.imgur.com/ZXZH8La.jpg) 4. 現在常是取得feints變數值,會先查詢當前活動中的getFeints字彙環境,由於我們沒有在getFeints函式中定義任何變數,所以這個字彙環境是空的。 ![](https://i.imgur.com/GrqA2bH.jpg) 5. 接下來會往當前字彙環境的外部環境搜尋,也就是Ninja環境,我們可以在這個字彙環境中找到feints變數,就搜尋完成了。 ![](https://i.imgur.com/ZrOnS4x.jpg) ### 5.6.2 留意私有變數 在JS中,可以把在一個物件上建立的屬性再指派給另一個物件。 #### ▌程式列表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; assert(imposter.getFeints () === 1, "The imposter has access to the feints variable!"); ``` 這裡利用`imposter.getFeints = ninja1.getFeints;`,將ninja1.getFeintsz方法指派給一個全新的imposter物件,而且當我們再imposter物件上呼叫getFeints函式時,是可以存取到ninja1的變數feints值。 這個例子說明了JS中沒有物件是私有變數的,但是我們透過物件方法來建立閉包,來作為替代方案(做出類似私有物件的變數)。 ![](https://i.imgur.com/BUqkjdR.jpg) ### 5.6.3 重新檢視閉包和回呼範例 #### ▌程式列表5.13 在timer回呼函式中使用閉包 ```javascript! function animateIt(elementId) { var elem = document.getElementById(elementId); var tick = 0; var timer = setInterval(function(){ 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"); ``` 1. 每次呼叫animateIt函式時,都會建立一個新的函式字彙環境。(這邊有兩個animateIt函式,所以就建立了兩個字彙環境) ![](https://i.imgur.com/zr44fxJ.jpg) 2. 上述新建立的兩個字彙環境,都追蹤了動畫一組重要的變數:elementId、elem(進行動畫處理的DOM元素)、tick(目前的片刻數)、timer(執行動畫的計時器ID值) ![](https://i.imgur.com/OvRY8yn.jpg) 3. 此例中,瀏覽器會讓setInterval裡的回呼函式一直持續著,直到我們呼叫clearInterval函式。 4. 到了指定的間隔時間,瀏覽器會呼叫對應的回呼函式,並且藉著閉包來存取在建立回呼時所定義的變數 ---> 藉由建立多個閉包,便能夠一次做出許多事情 ---> 每當有計時器到期,回呼函式會喚醒建立時所在的環境 ---> 每次回呼的閉包都會自動追蹤自己所擁有的變數 ![](https://i.imgur.com/sJ8LNHZ.jpg) ## 5.8 習題 **** ## 參考資料 * [[CS] 堆疊和堆積(Stack Memory and Heap Memory)](https://pjchender.dev/computer-science/cs-stack-heap/) * ==推== [Closures Explained in 100 Seconds // Tricky JavaScript Interview Prep](https://www.youtube.com/watch?v=vKJpN5FAeF4&list=PLJ1djWumFFcno2X_dXvfjupsdpHxzc55k&index=2&t=121s) * ==推== [Huli - 所有的函式都是閉包:談 JS 中的作用域與 Closure](https://blog.huli.tw/2018/12/08/javascript-closure/) * [[JavaScript學習系列]什麼是JavaScript的Scope,弄懂Scope的規範,才能避免不必要的bug](https://rollerblade.tw/javascript-scope/) * [Javascript Closure tutorial ( Closures Explained )](https://www.youtube.com/watch?v=71AtaJpJHw0) * [ 9.6: JavaScript Closure - p5.js Tutorial](https://www.youtube.com/watch?v=-jysK0nlz7A&list=PLJ1djWumFFcno2X_dXvfjupsdpHxzc55k&index=1)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully