突然有一點新的想法。
前言
上個月我有寫過一篇:談談 JS 中的 Closure(閉包),但最近又有了一些新想法,所以想在寫一次關於「閉包(Closure)」這個主題。
就跟標題一樣,我覺得閉包沒有那麼複雜,你只要先理解「作用域」、「作用域鏈」跟「回傳值」這三樣東西,我覺得就沒那麼複雜了。
區域作用域與全域作用域
簡單來說,作用域就是:一個變數的生存範圍
在 JavaScript 中,必須透過 function
才能建立出作用域:
1 | function scope() { |
這時候我們會說 scope
產生了一個「區域作用域」,而 a
是這個作用域中的「區域變數」。它只有在 scope
中可以被存取,外面沒辦法。所以 a
的生存範圍只在 scope
這個作用域裡面。
那全域作用域呢?很簡單,就是沒有宣告在 function 裡的變數:
1 | var a = 10 |
這時候在哪裡都能存取到 a
,因為 a
是「全域變數」,也就是生存範圍是「全域作用域」。
作用域鏈(Scope chain)
在理解區域作用域跟全域作用域後,要來理解什麼叫「作用域鏈」,舉個簡單的例子:
1 | function scopeA() { |
在這個例子裡面,我們有兩個作用域,分別是 scopeA
和 scopeB
。
先看 b
的部分,依照前面提的概念,b
的生存範圍只在 scopeB
中,所以只要出了 scopeB
,它就會 GG,所以 scopeA
去存取 b
的時候才會得到錯誤。
那 a
呢? a
的部分其實就是作用域鏈的機制。scopeB
在自己的作用域中找不到 a
,所以它就往上找,找到 scopeA
裡面的 a
,最後印出 a 的值。
這個「往上找」的行為就叫做「作用域鏈」,唯一要注意的一點是,它只能往上找,不能往下找。
關於回傳值
其實大部分的文章在告訴你作用域跟作用域鏈後,就會開始講什麼是閉包了。但是在那之前,我覺得還有一個很重要的東西不能忽掉:回傳值
一樣來舉點例子,按照作用域的概念,你沒辦法在全域空間中去存取區域變數,但如果真的要存取的話沒有任何辦法嗎?其實是有的,而且你一定也用過,那個方法就是透過「回傳值」:
1 | function scope() { |
這樣就可以拿到 a
的值了。而且不只是這樣,這裡還可以試著回傳物件或陣列看看:
1 | function scope() { |
1 | function scope() { |
最後如果變成回傳 function 呢?
1 | function scope() { |
把 function 放到最後才講是因為我覺得這樣比較好理解,如果一開始就拿 function 來舉例你可能會有點混亂,不知道為什麼可以在 function 裡面存取到 a
。因此才用這種一步一步來的方式,希望這樣會好理解一點。
所以你知道只要利用「回傳」,不管最後的回傳值是變數值、物件、陣列或函式, a
的值都會被保留下來,只不過 function 的例子比較特別一點,你要考慮前面提到的「作用域鏈機制」。還記得嗎?雖然 function 自己的作用域裡面沒有 a
但是它可以往上找,找到 scope
裡面的 a
,所以最後在執行 fn
的時候才能夠存取到 a
。
靜態作用域與動態作用域
不過你可能還有一個疑問:「fn
是在全域空間執行的,如果現在有個全域變數也叫 a
呢?」
1 | var a = 100 // => 新增一個全域變數 |
這個問題非常好,首先正確答案是 10
。
其實是這樣的,在 JavaScript 中一個 function 被宣告的時候,它的作用域就已經決定好了(特別強調 JavaScript 是因為有些程式語言不是這樣子)
所以按照這個規則,function 在被回傳的時候,它的作用域是這樣子:
1 | var a = 100 |
這樣應該就好理解多了吧?所以不論你之後在哪裡呼叫這個 function,它的作用域都不會變,這一定要搞清楚。
閉包
終於要來談今天的主題,但其實剛剛示範的例子裡就包含了閉包的概念:
1 | function scope() { |
不用懷疑,像 a
這樣被關在 function 裡面,只能透過 fn
來存取的這個行為就叫「閉包」。
我覺得這個機制本身沒有什麼,重要的是你要理解這是怎麼做到的,除了前面提到的「回傳值」外,還要理解「作用域」、「作用域鏈」才能真正理解原因。
所以接下來只示範一些閉包的經典題目,還有實際用途。
setTimeout 印出 i 的值
一個很經典的題目:
1 | for(var i=0; i<=5; i++) { |
最後的結果會是:
1 | 6 |
其實原因只是當 callback function 執行的時候,它自己的作用域裡面沒有 i
,所以依照作用域鏈往上找,最後找到全域空間的 i
,只是這時候迴圈已經跑完了,所以才會是 6
。
改寫的方式有很多,我列幾個比較常見的。
1. 在包一層 function
1 | function printNumber(num) { |
或改寫成 IIFE:
1 | for(let i=0; i<=5; i++) { |
2. 改用 ES6 的 let
let 的作用域是用 {}
來產生的,所以可以寫成:
1 | for(let i=0; i<=5; i++) { |
可以想成是這樣:
1 | { |
建立私有變數及公開方法
透過閉包的特性,你可以把變數藏在 function 裡面,讓外面的人沒辦法存取,並且只能透過公開(return)出去的東西來存取。
來簡單時做一個「開戶」的功能:
- 開戶時要先存 1000 塊
- 開戶後可以查詢餘額、領錢、存錢
1 | function createBank(money) { |
這邊簡單示範所以就沒做太多的額外處理了,方便看懂最重要。
所以以上這是關於閉包,還有閉包的用處。