不只是JavaScript,scope的概念在每個程式語言裡面都有,但每個語言scope運作邏輯多少有差異。
我們先來舉個例來了解scope:
隨著myFunction()
執行,這段程式毫無疑問的console.log()
出了myName
這個變數的值’my name’
。
但如果我們稍微調整console.log()
以及變數定義的位置,情況會變得有所不同。
如果我們將myName
的宣告寫在function內部,則在外面的console.log()
就無法取得myName
這個變數,所以會出現錯誤顯示myName
沒有被定義。
我們明明在function內定義了myName
,在外部卻不能使用,這就是因為它所在的scope和外部的scope不同,而且外部的scope不能存取內部的scope。它們的關係就像這樣:
比較小的scope就像一個小包廂,不能隨意進入。但包廂內的人隨時可以走出來看看外面。這就是為什麼在外面的console.log()
存取不到myName
,它不能隨意闖入別人的包廂。
以這個例子來說,function內{}
包起來的範圍就自己成為一個scope了,被稱為一個block scope。
不只是function可以形成scope,在大多數情況下,使用{}
包起來的範圍都會自成一個scope。我們刻意用if
結構來創造第二層的scope。
目前我們有兩個內部的scope,一個在function內,另一個在if
內。目前這樣寫也都是正常運作的,因為內部的scope可以存取外部的scope內容。另外,最外層被稱為global scope。
我們再稍微改寫一下這個例子:
我們多宣告了兩次myName
,並且在不同的scope裡都用console.log()
印出來。JavaScript會尋找同一個scope裡面有沒有myName
,沒有的話才會繼續往更外層(更大的scope)尋找。
在現在的情況裡,if
那一層scope有myName
會印出”my name 2”
、function那一層scope也有myName
會印出”my name 1”
,而global scope則會印出”my name”
。
請注意,這並不是內部的myName
覆蓋掉了外部的。用let
關鍵字表示要新增一個變數,就算新變數的名稱和先前定義過的一樣,在JS裡也是可行的。換句話說,在上面例子裡看到的3個myName
,就只是3個名字剛好一樣,毫無關係的變數。這是JavaScript scope跟變數的運作方式,在其他語言可能不適用。
如果想要複寫掉原本定義過的myName
的話,可以這樣寫:
可以注意到在if
那層的scope裡,少了let
這個關鍵字。這時候JS會做的就不是產生新的變數,而是尋找之前有沒有myName
,在同一個scope找不到,就會接著往上層scope找,然後更新它。
在這個例子中,我們保留了所有的console.log()
,由於myName
被更新了,就只會印出”my name 2”
這個值。這和上個例子其實就只差了let
,但結果完全不一樣!
let
和 var
常見的JavaScript宣告關鍵字除了let
還有var
,在比較早期的JavaScript裡面,開發者幾乎只能用var
來宣告變數,但後來推出了let
這個關鍵字。這兩個關鍵字有一些些微的差異,最重要而且最大的差別就是它們適用的scope不同。
let
如剛才提到的,是block scope,只要有新的{}
就會產生新的scope,var
是function scope,只有function內的{}
才會產生新的scope。
讓我們舉例說明。
我們同樣用剛剛製作一個function裡面在「用if
製作另一個scope」的情境為例。對insideIfLet
來說if
裡面是一個新的scope,新的包廂,所以在外面的console.log()
會失敗,它不能存取內部的變數。
但詭異的是,使用var
在這種情況下console.log()
卻可以正常的運作。原因是對var
來說,只有function的{}
才會開闢新scope,if
內的{}
不是。
好的我知道非常奇怪,不知道為什麼JS的設計師要這樣設計。其實簡單的建議就是不要用var
,很多情況下工程師很難直接判斷怎樣的{}
是來自function,判斷這件事也會花費很多不必要的時間。使用var
會造成scope變得更複雜,還幾乎沒有必要。相較之下,使用let
時只要看見{}
,就能判斷那是一個新的scope,對於自行回顧程式或溝通協作都非常有幫助。
在JavaScript中,還有一個容易讓人混淆的關鍵字叫this
,它的代表的值會隨著程式碼執行的環境(context)而改變。不同的context下,this
可能指向不同的對象,搞清楚這件事就是我們這個今天的目標。
我們先考慮這個user
object:
這個object叫做me
,裡面包含firstName
和lastName
兩個property,還有一個method叫做eat()
,它的功能單純就是印出一串string。
目前eat()
是寫死的,不能根據名稱動態的調整。我們現在希望可以讓eat()
存取到firstName
以及lastName
,這時候就可以用this
這個關鍵字。
而這正是this
被設計的目的 — 存取同一個object內的property或是method。滿好理解的,看起來沒什麼問題對吧?但問題就是這個this
的運作模式很多時候真的讓人混淆。
我們看看下面的例子。
我們在eat()
裡面再多定義一個functionhungry(),
定義完之後馬上使用它。
這時候卻會出現這個結果,this.firstName
是undefined
,我非常確定沒有打錯字。
這是因為this
指向的是「呼叫這個function的object」。以上面的例子來說me.eat()
就是由me
在呼叫eat()
,eat()
內用到的this
就是me
。但你可以看見hungry()
前面並沒有誰在乎叫它,它不是me.hungry()
或類似的寫法。這時候hungry()
的this
指的就是程式碼最外層的context(我們會稱作global context,在瀏覽器裡面是Window
這個object),反正不是me
。
關於Window
的資訊現在先聽聽就好,總而言之,在一個function前面,呼叫它的角色,就代表this
的值。現在你大概知道這個關鍵字有多擾人了。
順帶一提,如果我們是可以調整剛才那個hungry()
內的this
指向的。
用每個function內建的methodcall()
(bind()
、apply()
也可以),就可以指定this
的指向。
當然,我們還有其他辦法可以解決這個this
難題,之後會提到。
說了這麼多關於scope和context的內容有點讓人混亂,不過今天只是希望給大家一點概念,不是真的要熟悉每一個細節。重點是在JavaScript裡面,scope和context非常容易讓人混淆(特別是this
),在某些比較複雜的情境,資深人員也需要一點時間來搞清楚scope和context的問題。現在的目的是讓大家認識,並且在日後遇到找不出原因的bug的時,可以想到是scope和context的可能性。隨時都可以用console.log()
來確認得到的值是不是如預期。
明天我們會討論更多JavaScript裡常見的寫法與其特殊規則,正是因為有這些內容的存在,第一次看到JavaScript才容易不理解,在明天的內容之後,至少能夠看懂那些簡寫是發生了什麼事。
我是Erkin, 一個網站開發者。
有任何疑問或是想勘誤的話歡迎聯繫。