若還不清楚甚麼是提升Hoisting的話,可以先看看之前的介紹喔!因為文章內容有部分相關,所以必須先有 Execution Context 以及 Variable Object 的觀念後,會較能理解本篇的內容。
最常見的解釋是:作用域是一個變數的生存範圍,若於作用域之外將失去存取此變數的權限。
馬上來看一個範例:
在這段程式中,定義outer與inner兩個函式,並且都在自已的函式中宣告一個變數,但當我們執行到console.log (y) 時會出現Uncaught ReferenceError: y is not defined。
這其實是因為 y 並不在outer的作用域中,所以他沒有存取 y變數的權限。但為什麼inner卻能夠存取到outer的 x 變數呢 ? 這是因為在inner的作用域中找不到 x 變數,所以JavaScript Engine試著向外尋找 x 變數,最後在outer中找到,最後便能印出1443的訊息!
JavaScript採用的是靜態作用域,函式的作用域在函式定義時就決定了。 而靜態作用域相對的是動態作用域,函式的作用 域是在函式呼叫的時候才決定的。 讓我們看一個例子來理解靜態作用域和動態作用域之間的區別:
上面的程式碼中:
第1行,定義了一個value,並賦值為1;
第3~5行,宣告一個函式funB,函式的功能是列印 value 這個變數的值;
第7~9行,宣告一個函式funA,函式內部重新建立了一個變數 value 這個變數賦值為2;在函式內部執行了 funB() 這個函式;
第12行,執行 funA() 這個函式
JavaScript採用的是靜態作用域,以下僅是為了解析兩者流程上的差異!
假設JavaScript採用靜態作用域,讓我們解析執行過程:
執行funB函式,首先從 funB 函式內部查詢是否有變數 value ,如果沒有就根據撰寫的位置,查詢上面一層的程式碼,我們發現value等於1,所以結果會印出 1。
假設JavaScript採用動態作用域,讓我們解析執行過程:
執行funB函式,依然是從 funB 函式內部查詢是否有區域性變數 value。如果沒有, 就從呼叫函式的作用域,也就是從 funA 函式內部 查詢 value 變數,所以結果會印出 2。
用一句話解釋閉包:閉包就是一個作用域,當今天一個函式在返回一個作用域的時候,變數就會被存在作用域的裡面,不會被回收。
想必目前應該是有看沒有懂,讓我們用範例來解釋甚麼是返回一個作用域,甚麼是變數就會被存在作用域的裡面不會被回收!
馬上來看以下的範例,下方的outer(666)這時候到底是甚麼呢?Console.log它結果馬上揭曉!
原來這時的outer(666)返回了一個inner函式[作用域],剛剛的第一個問題返回一個作用域馬上獲得了解釋!
接下來,執行這段程式碼,神奇的事情發生了,那就是程式碼依舊輸出了 1443。
神奇在哪裡?神奇在一個 function 執行完成以後本來會把所有相關的資源釋放掉,可是我 outer 函式已經執行結束了,照理來說變數 x, y的記憶體空間也會被釋放,但我呼叫 inner 函式的時候居然還存取得到 x, y!
換句話說,x, y這兩個變數被「關在」inner 這個 function 裡面了,所以只要 inner 還存在的一天,x, y就永無安寧,只能一直被關 在裡面。
而事情的主因就是我在 function 裡面回傳了一個 function,才能造成這種明明執行完畢卻還有東西被關住的現象,而這種情形就是一 般人所熟知的閉包,Closure。
以上便解釋了變數就會被存在作用域的裡面不會被回收!
將上述的例子套上解釋閉包的一句話,便能有豁然開朗的感覺!
閉包就是一個作用域,當今天一個函式outer在返回一個作用域outer(666)的時候,變數x, y就會被存在作用域的裡面,不會被 回收,因此可以正常輸出1433。
Note:JavaScript Engine的垃圾回收機制會釋放不再使用的記憶體,但閉包為了保留函式和存取其語彙範疇的能力,就會予以保留, 不做記憶體回收。因此 inner 仍然保留指向 outer 內層範疇的參考,這個參考就是閉包。