作用域(Scoping)
對於Javascript初學者來說,一個最困惑的地方就是作用域;事實上,不光是初學者。我就見過一些有經驗的javascript程式設計師,但他們對scope理解不深。 javascript作用域之所以迷惑,是因為它程式語法本身長的像C家族的語言。我對作用域的理解是只會對某個範圍產生作用,而不會對外在產生影響的封閉空間。在這樣的一些空間裡,外部不能存取內部變量,但內部可以存取外部變數。
c語言的變數分為全域變數和局部變量,全域變數的作用範圍是任何檔案和函數存取(當然,對於非變數定義的其他c文件,需要使用extern關鍵字進行申明,使用static關鍵字也可以將作用範圍限定在目前文件中),局部變數的作用範圍就是從申明到最近的大括號涵蓋的區塊級範圍。 java則無全域變量,有類別變量,成員變數和局部變量,作用範圍根據public,protected,private等存取權限有不同的作用範圍,這裡就不多述。
JS作用域有哪些?
在ES5中,js只有兩種形式的作用域:全域作用域和函數作用域。
全域作用域其實是全域物件的作用域,任意地方都可以存取(如果沒有被函數作用域覆蓋)。
函數物件作用域跟c的局部變數作用域是不同的,它的作用域是整個函數範圍,不論他是在函數的任意位置申明的!這就是所謂的hoisting,也就是變數提升的概念。不過不急,以下會專門針對hoisting來解釋。
不過,在ES6中,新增了一個區塊級作用域(最近的大括號涵蓋的範圍),但是僅限於let方式申明的變數。
作用域示範:
定義變數時,如果不寫var,例如i=0,則會被定義為全域變量,作用域為全域作用域,否則為局部變量,作用域為函數作用域。上面第一行的var i=0,之所以說它是全域變量,是因為它已經是在全域區申明的了,並不在函數範圍內,因此跟 i=0 是一樣的。
至於,為什麼結果會是這樣,繼續往下看就知道了。
申明形式
變數宣告:
函數申明:
變數提
這題我面試過很多人,大多數人都說輸出的是日期。但真實的結果是undefined。為什麼是這樣呢?這裡就引出了一個概念--hoisting,中文的意思是變數提升。 MDN中對變數hoisting的解釋是這樣的:
var hoisting
所以,這樣就應該理解了,console輸出的時候,tmp變數只是申明了但未定義,所以輸出應該是undefined。
這裡需要說明的是,雖然所有的申明(包括ES5的var、function,和ES6的function *、let、const、class)都會被提升,但是var、function、function *和let、const、class的的提升卻不相同!具體原因可以看這裡的說明(大體的意思是雖然let,const,class也被提升了,但是卻並不會被初始化,這時候去訪問他們則會報ReferenceError異常,他們需要到語句執行的時候才會被初始化,而在被初始化之前的狀態叫做temporal dead zone)。我們來看一段程式碼就知道了:
這裡a被提升,但因為定義在後,所以輸出undefined
這裡a雖然被提升,但卻報了引用錯誤!
之所以或這樣
因為這樣的原因,推薦的做法是在申明變數的時候,將所用的變數都寫在作用域(全域作用域或函數作用域)的最頂上,這樣程式碼看起來就會更清晰,更容易看出來那個變數是來自函數作用域的,哪個又是來自作用域鏈(本文不對此多做解釋,請讀者自行百度,有機會再補充說明)。
重複聲明
上面的輸出其實是:1 2 2。雖然看起來裡面x申明了兩次,但上面說了,js的var變數只有全域作用域和函數作用域兩種,且申明會被提升,因此實際上x只會在最頂上開始的地方申明一次,var x=2的申明會被忽略,只用於賦值。也就是說上面的程式碼其實跟下面是一致的。
函數和變數同時提升的問題
如果是函數和變數類型同時申明定義了,會發生什麼事呢?看下面的程式碼
上面的輸出結果其實是: function foo(){} ,也就是函數內容。
而如果是這樣的形式呢
它的輸出卻變成:undefined
為什麼會這樣呢?
原來函數提升分為兩種:
一種:函數申明。就是上面A,function foo(){}這種形式
另一種:函數表達式。就是上面B,var foo=function(){}這種形式
第二種形式其實就是var變數的宣告定義,因此上面的B輸出結果為undefined應該就能理解了。
而第一種函數申明的形式,在提升的時候,會被整個提升上去,包括函數定義的部分!因此A跟下面的這種方式是等價的!
原因是因為:1、函數宣告被提升到最頂上;2、申明只進行一次,因此後面var foo='i am text'的申明會被忽略。
且函數申明的優先權優於變數申明,所以以下形式的輸出,同樣是函數內容:
總結
要徹底理解JS的作用域和Hoisting,只要記住以下三點即可:
1、所有申明都會被提升到作用域的最頂上
2、同一個變數申明只進行一次,且因此其他申明會被忽略
的申,且函數宣告會連帶定義一起被提升注意:透過with語句,可以暫時改變運行期上下文的作用域鏈,此時的對非var定義的變數進行訪問,會先訪問with中物件的屬性,然後才會向上順著作用域鏈向上檢查該屬性。