JavaScript之變數及作用域

南波發表於2018-10-13

本文共 1700 字,讀完只需 7 分鐘

概述

變數,程式語言中我們用來模擬現實概念的工具,比方說,變數可以表示物件,陣列,數字,字元。既然是工具,那麼就用工具的適用範圍,這個工具在這個適用範圍中才有效,在程式語言中,我們稱這個適用範圍叫作用域(scope)

本文會總結 JS 中作用域的相關概念。

  1. 什麼是作用域
  2. 全域性作用域
  3. 函式作用域
  4. 塊級作用域
  5. 詞法作用域(靜態作用域)
  6. 作用域鏈

一、什麼是作用域?

作用域, 英文意思是 scope, 我自己的話來理解就是:

變數訪問規則的有效範圍

  1. 作用域外,無法引用作用域內的變數
  2. 離開作用域後,作用域的變數的記憶體空間會被清除,比如執行完函式或者關閉瀏覽器。

二、全域性作用域

先看一段程式碼:

foo = "bar";
console.log(window.foo);  // bar
複製程式碼

在瀏覽器環境中宣告變數,該變數會預設成為全域性 windows 物件的屬性。

再看下面這段程式碼:

function foo() {
    name = "bar"
}
foo();
console.log(window.name) // bar
複製程式碼

在函式中,如果不加 bar宣告一個變數,那麼這個變數會預設被宣告為全域性變數,如果是嚴格模式則會報錯。

全域性變數可以在任何地方訪問到,但是有很大的問題存在。

全域性變數會造成命名汙染,如果在多處對同一個全域性變數進行操作,那麼就會覆蓋全域性變數的定義。同時全域性變數數量過多,非常不方便管理。

這也是為什麼像jQuery 和 underscore 這樣的類庫,要在全域性建立 $ 和 _ 變數,其餘私有方法屬性掛載到該全域性變數下。

三、函式作用域

JS 是函式作用域,在函式中定義一個區域性變數,那麼該變數只可以在該函式作用域中被訪問。

function doSomething() {
    var thing = "吃早餐";
}

console.log(thing);  // Uncaught ReferenceError: thing is not defined
複製程式碼

巢狀函式作用域:

function outter() {
    var thing = "吃早餐";
    function inner() {
        console.log(thing);
    }
    inner();
}

outter();  // 吃早餐
複製程式碼

在外層函式中,巢狀一個內層函式,那麼這個內層函式可以向上訪問到外部作用域的變數。

那麼,既然內層函式可以訪問到外層函式的變數,那麼把內層函式返回後呢?

function outter() {
    var thing = "吃晚餐";
    
    function inner() {
        console.log(thing);
    }
    return inner;
}

var foo = outter();
foo();  // 吃晚餐

前面我們提到了,函式執行完後,函式作用域的變數會被垃圾回收,以上程式碼可以看出當我們返回了一個訪問了外部函式變數的內部函式,最後外部函式的變數得以儲存。

這種當變數存在的函式已經執行結束,但仍在可以訪問的方式就是`閉包`。

閉包的具體實踐,後續文章會詳細說明。
複製程式碼

四、塊級作用域

JS 在 ES6 之前只有函式作用域,沒有塊級作用域的概念。
看一下程式碼:

function doSomething() {
    for (var i = 0; i < 10; i++) {
        ...
    }
    console.log(i)
}
doSomething();  // 10
複製程式碼

由於 JS 沒有塊級作用域,變數 i 在函式作用域中只有一個,每次 for 循壞都在改變這一個變數。

再看阮一峰老師 ES6 教程裡的一段程式碼:

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6]();   // 10;
複製程式碼

以上程式碼中,由於沒有塊級作用域,i 變數全域性只有一個,當 for 循壞結束,變數 i 的值等於 10, 所以 a[6]() 對應函式內的變數 i 的列印值就是 10。

ES 6 中通過 letconst關鍵字 引用了塊級作用域的概念,所謂塊級作用域,就是以 {}包裹的區域。

我們將阮一峰老師 ES6 教程裡的一段程式碼改成 let 的形式:

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6]();   // 6;
複製程式碼

這時,陣列內的索引為6函式內的變數列印值為6,每次迴圈,會建立新的塊級作用域,然後重新宣告一個新的變數 i;JS 的解釋引擎會記住上次迴圈的變數值,所以能夠返回正確的結果。

letconst 會宣告一個塊級作用域的變數及常量,不易發生變數命名汙染的問題,能規避衝突,幫助你寫出簡潔優雅的程式碼,建議一直使用。

五、詞法作用域(靜態作用域)

詞法作用域,也可以叫做靜態作用域,是什麼意思呢?

無論函式在哪裡呼叫,詞法作用域都只由函式被宣告時所處的位置決定。

既然有靜態作用域,那麼也有動態作用域。

而動態作用域的作用域則是由函式被呼叫執行的位置所決定。

var a = 123;

function func1() {
    console.log(a);
}

function func2() {
    var a = 456;
    func1();
}

func2(); // 123
複製程式碼

以上程式碼,最後輸出結果 a 的值,來自於 func1 宣告時所在位置訪問到的 a 值 123。

所以 JS 的作用域是靜態作用域,也叫詞法作用域。

六、作用域鏈

在 JS 引擎中,通過識別符號查詢識別符號的值,會從當前作用域向上尋找,直到作用域找到第一個匹配的識別符號為止。就是 JS 的作用域鏈

如果巢狀作用域有多個相同識別符號,那麼,最內部的識別符號會覆蓋外層識別符號,這叫做“遮蔽效應”

var a = 1;
function func1() {
    var a = 2;
    function func2() {
        var a = 3;
        console.log(a);  // 3
    }
    func2();
}

func1(); // 3
複製程式碼

func2 中變數 a,會從內部開始向外部上層尋找,找到最近的 a 識別符號的宣告為止。

總結

JS 是一門基於詞法作用域(靜態作用域)的語言,JS 會沿著作用域鏈像氣泡一樣向外部尋找變數宣告。

JS 又是函式作用域的語言,在 ES6 中,使用 letconst 關鍵字後,能讓變數處於塊作用域中,而且不存在宣告提升。

後面的文章會介紹 JS 中的宣告提升和閉包,敬請期待。

歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。

JavaScript之變數及作用域

掘金專欄 JavaScript 系列文章

  1. JavaScript之變數及作用域
  2. JavaScript之宣告提升
  3. JavaScript之執行上下文
  4. JavaScript之變數物件
  5. JavaScript原型與原型鏈
  6. JavaScript之作用域鏈
  7. JavaScript之閉包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值傳遞
  11. JavaScript之例題中徹底理解this
  12. JavaScript專題之模擬實現call和apply

相關文章