「作用域就是一個變數的生存範圍,一旦出了這個範圍,就無法存取到這個變數」。
當我們把變數 a
宣告在 function 中,function 之外的地方都無法取用這個變數:
但當我們把變數 b
宣告在全域,function 裡面也可以取用外面的變數:
再看看這個例子:
y
不能被 outer
取用:y
不在 outer
function 的作用域中因此無法被 outer
取用x
可以被 inner
取用:inner
在自己的作用域中找不到 x
,便開始向外尋找,最後在 outer
的作用域中取用 x
結論:
ReferenceError: variable is not defined
錯誤訊息Javascript 的作用域是採用「靜態作用域(static scope)」,也稱作「語法作用域 (lexical scope)」(也有人翻成「詞法作用域」、「語彙作用域」)。「靜態作用域」是相對於「動態作用域(dynamic scope)」而來。
靜態作用域決定作用域的方式是根據函式在哪裡被宣告,與函式在哪裡被呼叫無關,查找變數的流程不會因為函式實際執行的位置不同而改變。
動態作用域決定作用域的方式是以呼叫堆疊 (call stack) 為準,查找變數的流程是執行時才決定的。
來看看這個例子:
答案是 Amy。
init()
函式在自己的作用域建立了局部變數 name
以及 displayName()
函式displayName()
函式的作用域中沒有局部變數 name
,因此開始向外層尋找,先找到了 init
作用域的 name
變數 –> 因此答案是 Amy再看看另一個例子:
答案是 Peter。
displayName
裡的 name
其實就是 global 的 name
,跟 init
裡的 name
沒有關係。displayName()
是在全域中被宣告,即使是在 init()
中被呼叫,它的作用域仍與 init()
無關。(在某些採用「動態作用域」的程式語言中, name
確實有可能會是 "Amy"
)。這邊可以知道:
作用域有三種:
Javascript 執行的一開始就會創建一個全域環境,被定義在 function-scope 或是 block-scope 以外的變數就叫做全域變數 (global variable)。
每建立一個函式就會創建一個新的函式作用域。
var
, let
, const
宣告的變數,當他們在函式中宣告時都屬於函式作用域在 ES6 中引入了 let
與 const
變數,這兩個變數的宣告方式提供了「區塊作用域」。
{}
(例如 if 或 for)var
宣告的變數不會有區塊作用域,透過 let
與 const
宣告才能讓變數具有區塊作用域在上述的例子中,
foo1()
使用了 const
宣告變數,所以 if 的 {}
內為區塊作用域,const user = "花爸"
只存在於該 {}
區塊中。既便是同函式中的 console.log(user)
也無法取用 if {}
內宣告的變數。foo2()
使用了 var
宣告變數,不會有區塊作用域,var user = "花媽"
存在於 foo2
函式中。因此同函式中的 console.log(user)
可以取用該變數。Javascript 在使用一個變數的時候,會先在當層的作用域尋找該變數。若當前的作用域找不到該變數,會再往父層作用域尋找,就這樣一層一層往上找,一直到全局作用域如果還是沒找到就會報錯。這種由內而外搜尋的行為就是「作用域鏈」。
事實上,每個函式在執行時都會建立一個對應的作用域鏈。(延伸閱讀:前端中階:JS令人搞不懂的地方-Closure(閉包))
上述例子中,inner
函式在自己的作用域中找不到 a
變數,就會往上一層的 outer
作用域找,如果還是找不到就會再往上一層找直到最上層的 global
作用域為止,如果最後還是找不到就會報錯。
查找方向:【 inner function scope 】 -> 【 outer function scope 】 -> 【 global scope 】,這個過程就構成了一個作用域鏈。
對 inner
來說,a
變數並不存在於它的作用域中,但它仍能存取這個變數,這樣的變數也叫做「自由變數 (free variable)」。
根據 MDN 的敘述:
閉包(Closure)是函式以及該函式被宣告時所在的作用域環境(lexical environment)的組合。
當我們呼叫 outer
時,就只是執行內部的一個 function inner
而已。
但如果我們不直接執行 inner
,而是直接把這個函式回傳:
原本當函式執行完成以後,裡面宣告的變數也應該要被釋放,因此當 outer()
執行結束時,照理來說變數 greet
的記憶體空間會被釋放,但緊接著在呼叫 newFunc
時卻仍能存取到 greet
。
換句話說只要 newFunc
還在, greet
就依然存在。這種在 function 裡回傳了一個 function 導致明明函式執行完畢卻還能存取到資源的現象就是「閉包」。
我們首先在 outer
函式中:
greet
inner
函式,inner
函式會印出 greet
inner
函式接著把 outer
函式指派給 newFunc
變數,此時:
newFunc
是 inner
在 outer
運行時所建立的實例 (instance)newFunc
存取了 outer
回傳的值(也就是 inner
函式)inner
的 instance 中保有了原本作用域的環境參照,而作用域裡含有 greet
變數,因此調用 newFunc()
時 greet
也能被取用可以把程式中需要重複執行的部分透過閉包封裝起來,進一步簡化程式,或是讓變數的值給保存下來。
假設今天這個函式需要創造一個很大的 array,並且這個函式會被呼叫很多次:
每執行一次函式都需要重複產生一個 7000 個 index 的 array,太浪費資源了,這時就很適合使用 closure:
heavyDutyClosure
assign 給變數 getHeavyDuty
,讓 getHeavyDuty
去存取 heavyDutyClosure
所回傳的值(也就是函式 function(index)...
)getHeavyDuty
時,就是在呼叫函式 function(index)...
值得注意的是,雖然呼叫了三次 getHeavyDuty
但用 closure 寫法的版本只會在第一次印出 created!
執行第一次 getHeavyDuty
時的運作流程:
執行第一次後 function(index)...
就已經被存取 getHeavyDuty
這個變數了。後續在執行 getHeavyDuty
就是在執行 function(index)...
了。而 heavyDutyClosure
中沒有被 return 的部分(產生 7000 個 array、印出 created!)就只會執行一次。
接下來幾次執行 getHeavyDuty
的運作流程其實只剩:
封裝是物件導向程式設計(Object Oreinted Programming,簡稱 OOP) 一個非常重要的概念。
有些資料如果不想讓外部函式/方法改動它,就可以使用閉包的方式來確保只有內部函式可以改動內部資料。
這裡直接在全域宣告 count
變數是錯誤的,可能會導致程式碼出現不可預期的錯誤。比如要是有人在其他地方寫了 count = 5
資料就亂掉了。
這種情況就可以使用閉包,寫一個 function 把 count
這個變數和能夠改變這個變數的 function 封裝在一起:
這樣一來除了 .increment()
、.decrement()
、.getCount()
之外的方法都無法改變 count
這個變數。
count
是一個 private variablecount
只存在於 createCounter
作用域中count
只能被內部的函式改動資料,不會被外部環境所影響假設頁面上有 3 個按鈕,預期的行為是點擊第一個按鈕跳出 1 、點擊第二個按鈕跳出 2 ⋯以此類推。但實際操作後會發現,無論點擊哪一個按鈕都會印出 3。
原本想像中的迴圈應該是要這樣跑的:
但實際上 JS 引擎在運作時是這樣跑的:
在跑迴圈的時候只是加上它的 callback function 而已還沒有執行,是要等到使用者按按鈕的時候才會去尋找 i
變數。
也就是說,事件發生時(使用者點擊按鈕)所引發的函式 (callback function) 是在迴圈跑完之後才執行。
加上這幾個 callback function 本身並沒有 i
這個變數,因此會向作用域的外層開始尋找,一直找到在迴圈中定義的 i
(定義在全域中)作為其值。
而此時的 i
早就已經變成了 2 了,因此無論點擊哪一個按鈕,取用的都是同一個全域變數中的 i
,所以都只會輸出 3。
這個問題可以透過閉包來解決:
建立一個新的函式並傳入參數:
透過高階函式 (Higher Order Function) 產生三個新的 function,並且因為傳入了一個參數 i
,利用閉包的特性將 i
個別鎖進 getAlert
中。
或者也可以利用 IIFE(Immediately Invoked Function Expression):
透過 IIFE 把 function 包起來並傳入參數 i
立即執行。
迴圈每跑一次就會產生一個新的 function 並且立刻呼叫它,因此會就地產生新的作用域,並且利用了閉包的特性將參數 i
鎖住。
也可以直接把 var
改成 let
(ES6):
ES6 裡有了塊級作用域 (block scope) 之後就可以直接利用 let
的特性,等於每跑一次迴圈都會產生一個新的作用域,因此 i
就會存在在該作用域裡。可以看成這樣: