Scope 是什麼?
Scope 中文翻作「作用域/範疇…」代表了變數可以被取用的範圍,如果嘗試在 scope 外存取變數,就會發生錯誤。
下面的範例就是因為在 localText
的 scope 外存取 localText
,所以發生了錯誤。
1 | function hello() { |
那麼 Scope 的範圍有哪幾種呢?
- Global Scope:在任何地方都能存取此變數。
- Function Scope:在 function 內部能存取此變數。
- Block Scope:在 block{}內部能存取此變數。
- Module Scope:
向外找的特性
知道變數的 Scope 範圍,有助於幫我們判斷現在存取的變數究竟是誰。
在區域範圍內時,可以讀到自己區域的變數以及外面區域的變數,但全域的地方無法讀到區域的變數。
為了幫助理解以下的程式,在這邊先知道用
var
宣告的變數,若在 function 外宣告,作用域為 global;在 function 內宣告,作用域在 function 內。
後面會有更詳細的介紹。
看一下這個範例:
1 | var globalVariable = "G"; // 作用域為全域 |
在 test()
內可以讀到外面的 globalVariable
,但 global 區域沒辦法讀到 test()
內的變數,因此會報錯。
所以簡單來說在存取變數的時候,會先在區域內找這個變數,如果找不到再向外面一層找。
如果變數有重名的情況,會先在小區域找,找不到的話才會再往外找:
1 | var username = "Cathy"; // 作用域為全域 |
第一個 console.log
在 callName()
內找得到 username
這個變數,所以印出 "Winni"
。
第二個 console.log
在 callName()
外面,不會往 callName
內讀取變數,所以他印出外面的全域變數 "Cathy"
。
如果有多個 function 也是一樣,會先在小區域找,找不到的話再往外找:
1 | var username = "Cathy"; // 作用域為全域 |
這個範例,test()
裡面找不到 username
,所以向外一層在 callName()
中找到了,印出 Winni
。
接下來出一個小題目,讓大家自己判斷一下 console.log
會印出什麼呢?
1 | var username = "Cathy"; // 作用域為全域 |
.
.
.
.
.
答案是:"Joy"
因為 test()
function 本身就找得到 username
,所以印出來的就是宣告在 test()
裡的 username
,其值為 "Joy"
。
第二題小測驗
1 | var username = "Cathy"; // 作用域為全域 |
.
.
.
.
.
答案是:"Winni"
第六行修改的 username
為全域變數的 username
,而非宣告在 callName()
裡的 username
,所以 callName()
裡的 username
值不會改變。
而 console.log
在 call()
中找得到 username
,所以會印出 Winni
。
第三題小測驗
1 | var username = "Cathy"; // 作用域為全域 |
.
.
.
.
.
答案是:Winni
callName()
中找不到 username
,所以他向外一層找到了全域作用域的 username
,並將其值修改成 Winni
,因此 console.log
印出來的值為被修改過的 Winni
。
從上面幾個範例,只要我們知道變數的作用域是哪裡,就能判斷存取的變數究竟是哪個變數了。
但是,我們要怎麼知道變數的作用域是哪裡呢?
用 var 宣告變數
如果在 function 外用 var
宣告的變數,那麼該變數就是的 scope 是 global,在任何地方都能存取。
1 | var group = "GFRIEND"; // 作用域為全域 |
如果在 function 內用 var
宣告的變數,那麼該變數的 scope 是 function,在 function 內部才能存取。
1 | function getName() { |
第二個 console.log
因為無法讀取到 function 內部宣告的變數,所以會報錯。
Function Scope 的問題
問題一:範圍太大,可能無意間造成覆蓋。
1 | function test() { |
雖然上面的程式碼看起來是宣告了兩個變數,但因為是在同一個 function scope 內宣告兩次 count
變數,因此兩個變數實際上是同一個變數,在修改 if
裡面的 count
值時,外面的 count
同時也被修改了。
這顯然不太符合我們的預期。
問題二:
1 | for (var i = 0; i < 5; i++) { |
正常我們應該預期結果要印出 1,2,3,4,5,但實際上會印出 5,5,5,5,5。
因為我們以為,程式是這樣運作的:
1 | for (...){ |
但實際上卻是:
1 | for (...){ |
因為 i
這個變數不在 function 裡面宣告,所以是 global 範圍的變數。當 setTimeout()
function 倒數完一秒要印出 i
時, 是迴圈跑完的時候, i
的值已經變成了 5
。
諸如此類的問題,讓我們使用起來有些不便,到了 ES6 之後,新增了 block scope 的概念。
用 let 和 const 宣告的變數
在 ES6 版本,有了新的宣告變數的方式: let
和 const
,使用這兩種方式會以 block 來區分作用域的範圍,也就是用 {}
來區分的意思。
在 block 之外使用 let
和 const
宣告的變數,如同在 function 外使用 var
宣告的變數一樣,都是 global scope 的,在任何地方都能使用。
1 | let count = 1; |
在 block 內用 let
和 const
宣告的變數,scope 為 block 內部。
修改 {}
內的 name
,並不會影響到外面的 name
:
1 | let name = "John"; |
更常的情況,我們會應用在 if
判斷式或是 for
等迴圈裡。
判斷是和迴圈的括號也算在 block 的範圍內,像是
for(let i=0; i<10; i++){}
的i
。
block 內用 let
宣告的變數,不會影響到 block 外的變數。
1 | let count = 1; |
在同一個 block scope 用 let
或 const
重複宣告變數的話,會發生錯誤:
1 | if (count > 0) { |
如果是在迴圈裡用 let
或 const
宣告變數的話,每一次迴圈的是一次新的 block scope。
所以在迴圈裡宣告變數,並不會算重複宣告變數:
1 | for (var i = 0; i < 5; i++) { |
會印出五次 0
。
也可以解決上面用 var
宣告的問題二,原本的程式是這樣:
1 | for (var i = 0; i < 2; i++) { |
會印出 2,2
但我們改成用 let
宣告的話,就可以正常印出 0, 1
:
1 | for (let i = 0; i < 2; i++) { |
這個例子我其實理解了很久才懂,因為每一次迴圈都是新的 block scope,所以 i
的值並不會被修改到。
迴圈跑第一次的時候程式看起來像是這樣:
1 | for (...) { |
第二次則會變成:
1 | for (...) { |
兩次迴圈的 i
,是獨立互不相干擾的變數,所以就能正常的印出 1, 2
。
靜態作用域
在討論靜態作用域之前,大家先來思考一下,在哪裡呼叫 function,會不會影響到變數的值?
我們看下面這個範例:
1 | var name = "Cathy"; |
我們在 callName()
裡面找不到 name
變數,所以要向外一層找找看有沒有 name
。
那麼這個外面一層,究竟是定義 function 時的外面 —— 也就是 global 區域,還是呼叫 function 時的外面 —— 也就是 test()
區域呢?
答案是 global 區域,所以 console.log(name)
印出來應為 "Cathy"
。
因為在撰寫時變數的作用域就已經確定好了,所以稱之為「靜態作用域」。
總結
關於 scope 這個主題,原本處於一個大概知道在做什麼的了解,但仔細要寫起來,也發現了一些原本沒有注意到的細節。像是「靜態作用域」就是在寫這篇文章時,第一次學到的知識。
這篇文章來來回回改了很多遍,因為總是會有沒有先講A介紹B的時候會不清楚,但沒有先講B介紹A也會有點模糊等雞生蛋蛋生雞的問題,希望我最後呈現的內容能讓人好懂。