撰寫程式一定會非常頻繁的使用變數,之前介紹過Web的生命週期,變數也有生命週期,或者是說作用域更為適合。
什麼是作用域(scope)?意思就是,變數在程式中可以被存取的範圍。
以範圍來區分變數的話,可分為區域變數,全域變數。
那區域跟全域又是以什麼為界定來區分的呢?
傳統的後端程式語言,區域變數只在大括號{ }中有效,在此範圍外,就是全域變數,它是以區塊(block){ }作為區分,但JavaScript卻不是以此做區分,它是用函式(Function)作為區分,在函式內為區域變數,函式外為全域變數,要達成這樣的條件還有一個前提,變數必須以var宣告。
var scope = '全域';
function getValue() {
var scope = '區域';
return scope;
}
console.log(getValue()); //區域
console.log(scope); //全域
結果顯示,函式回傳的scope是區域變數的值,最後的敘述式(statement)回傳的是全域變數的值,由此可知它們是不同的變數,即使名稱一樣。
var scope = '全域';
function getValue1() {
var scope = '區域1';
return scope;
}
function getValue2() {
var scope = '區域2';
return scope;
}
console.log(getValue1()); //區域1
console.log(getValue2()); //區域2
console.log(scope); //全域
getValue1( )與getValue2( )裡面都有scope,不同區域範圍的同名變數,實際上是不同的變數。
即使scope被宣告3次,但彼此之間沒有關聯,都是獨立的變數。
如果不用var宣告呢?
scope = '全域';
function getValue() {
scope = '區域';
return scope;
}
console.log(getValue()); //區域
console.log(scope); //區域
結果顯示同一個變數,區域變數。
在JavaScript中即使不以var宣告,一樣可以執行,但就算在涵式內也視為全域變數,更進一步來說,是window全域物件的屬性。
如此會汙染到我們的全域物件,這不是一個好現象,設計程式應養成好習慣,要加上宣告關鍵字。
剛剛談過了,在JavaScript中,變數是以函式區塊做為區域跟全域的區別。
if (true) {
var x = 10;
}
console.log(x); //10
變數x為全域變數。
以上的範例如果出現在傳統的後端程式語言中,會發生錯誤,但在JavaScript是可以正常執行的。
ES6新增了宣告關鍵字:let,let支援區塊範圍。
if (true) {
let x = 10;
}
console.log(x); //Uncaught ReferenceError: x is not defined
如此一來,在區塊外面,就看不到區塊內的變數了。
在開發Web的時候,盡可能的不要汙染到全域物件,才是良好的習慣。
每個變數都有自己的作用域範圍,不論是全域或是區域,在自身的作用域範圍內,可以被存取,除此之外,也可以透過作用域鍊的關係,讓變數可以在其他的作用域範圍被存取。
function a() {
let val = 2;
b();
}
function b() {
console.log(val);
}
let val = 1;
a();
以上的範例,輸出的值是多少?
相信很多人看到
let val = 2;
b();
會選擇答案是2,但實際上是1。
Why?按照程式的執行順序上來看,宣告val=2後,直接呼叫b( ),但b( )沒有val這個變數,所以它會往呼叫b( )的上一行找到val=2,但答案很顯然地,輸出的是全域變數。
在JavaScript中的執行環境有分全域執行環境與區域執行環境,函式內部是區域環境,全域環境是我們一開始在執行JavaScript,瀏覽器所建立的環境,也是最外層的環境。
從上面的範例來看,a( )、b( )、變數val都是宣告在全域環境中,它們位於同一層的執行還境,a( )裡面的變數val是區域變數,是執行環境為a( )的區域變數。
了解函式或變數本身所在的執行環境(execution contexts)後,我們來看看b( )裡面的val,在b( )中,並沒有宣告val,這表示如果想取得val的值,必須得往其他的區域找,那為何它不找a( )裡面,卻找全域環境?
這是因為,JavaScript引擎如果在該執行環境找不到變數,它會往上一層找。既然a( )、b( )都是同一層,那當然不會進入函式尋找,b( )的上一層是全域環境,所以JavaScript引擎會在全域環境中找到了val,以上才是真正的執行過程。
或許有人會認為在a( )中呼叫b( ),那b( )才會往a( )找val,我們再仔細看看範例,b( )是定義在全域環境中,a( )只是呼叫b( )而已,並沒有在函式主體中定義它,重點是函式定義何處,而不是誰呼叫它。
所以我們可以這麼說,b( )透過作用域鏈的關係,在它的上一層(全域執行環境)能找到val。
我們再看另一個例子:
function a() {
let val = 2;
function b() {
console.log(val);
}
b();
}
let val = 1;
a(); //2
眼尖的讀者會發現,這是閉包(closure),關於閉包,之後會討論。
我們先來看看,在a( )中宣告b( ),b( )的上一層就是a( ),當它需要取得val的值,自然會透過作用域鏈在它的上一層,a( ),找到val的值。
參考資料:
JavaScript 全攻略:克服 JS 的奇怪部分 範圍鏈