更新於 2024/11/04閱讀時間約 9 分鐘

Day08 JavaScript Scope & Context


Scope(作用域

不只是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的話,可以這樣寫:

前面沒有新的let,JS尋找之前定義過的myName並且覆蓋掉它。

可以注意到在if那層的scope裡,少了let這個關鍵字。這時候JS會做的就不是產生新的變數,而是尋找之前有沒有myName,在同一個scope找不到,就會接著往上層scope找,然後更新它。

這種情況下,所有的myName都被更新成"my name 2"。

在這個例子中,我們保留了所有的console.log(),由於myName被更新了,就只會印出”my name 2”這個值。這和上個例子其實就只差了let,但結果完全不一樣!


letvar

常見的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內的{}不是。

用let時,if{}被視為新的scope,而外部不能存取內部scope,所以出錯。


好的我知道非常奇怪,不知道為什麼JS的設計師要這樣設計。其實簡單的建議就是不要用var,很多情況下工程師很難直接判斷怎樣的{}是來自function,判斷這件事也會花費很多不必要的時間。使用var會造成scope變得更複雜,還幾乎沒有必要。相較之下,使用let時只要看見{},就能判斷那是一個新的scope,對於自行回顧程式或溝通協作都非常有幫助。

Context

在JavaScript中,還有一個容易讓人混淆的關鍵字叫this,它的代表的值會隨著程式碼執行的環境(context)而改變。不同的context下,this可能指向不同的對象,搞清楚這件事就是我們這個今天的目標。


我們先考慮這個userobject:

這個object叫做me,裡面包含firstNamelastName兩個property,還有一個method叫做eat(),它的功能單純就是印出一串string。


目前eat()是寫死的,不能根據名稱動態的調整。我們現在希望可以讓eat()存取到firstName以及lastName,這時候就可以用this這個關鍵字。

而這正是this被設計的目的 — 存取同一個object內的property或是method。滿好理解的,看起來沒什麼問題對吧?但問題就是這個this的運作模式很多時候真的讓人混淆。


我們看看下面的例子。

我們在eat()裡面再多定義一個functionhungry(),定義完之後馬上使用它。

這時候卻會出現這個結果,this.firstNameundefined,我非常確定沒有打錯字。


這是因為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指向的。

我們指定讓me來callhungry(),它裡面的this就會是me了。

用每個function內建的methodcall()bind()apply()也可以),就可以指定this的指向。


當然,我們還有其他辦法可以解決這個this難題,之後會提到。

小結

說了這麼多關於scope和context的內容有點讓人混亂,不過今天只是希望給大家一點概念,不是真的要熟悉每一個細節。重點是在JavaScript裡面,scope和context非常容易讓人混淆(特別是this),在某些比較複雜的情境,資深人員也需要一點時間來搞清楚scope和context的問題。現在的目的是讓大家認識,並且在日後遇到找不出原因的bug的時,可以想到是scope和context的可能性。隨時都可以用console.log()來確認得到的值是不是如預期。


明天我們會討論更多JavaScript裡常見的寫法與其特殊規則,正是因為有這些內容的存在,第一次看到JavaScript才容易不理解,在明天的內容之後,至少能夠看懂那些簡寫是發生了什麼事。


Resource

今日Codepen

連結

Credits

關於我

我是Erkin, 一個網站開發者。
有任何疑問或是想勘誤的話歡迎聯繫。

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.