美思 現代 [JavaScript] 程式設計:JavaScript 程式的範疇 (Scope)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在 JavaScript 中,範疇 (scope) 是比較容易造成 bug 的特性之一。這是因為在 ES6 之前的 JavaScript 沒有區塊範疇 (block scope) 的概念,很多我們在別的語言視為理所當然的事情在 JavaScript 卻是造成 bug 的潛在來源。

在 ES6 之後,才用新語法特性補足了範疇相關的議題。藉由在專案中引入 Babel,即使要維護舊專案,也可以用較佳的新語法特性來處理範疇的議題。

JavaScript 原本沒有區塊範疇 (Block Scope)

我們來看一個應該報錯卻「正確」執行的範例程式:

for (var i = 0; i < 10; i++) { /* Pass. */ }

assert(i === 10, "i should be 10");

// Home-made `assert`
function assert (cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

在主流語言中,計數器 i 離開 for 迴圈後,因超出 for 迴圈的區塊,理應在程式中消失。但在 JavaScript 程式中,i 卻保留了下來。像是 ij 這類計數器常用的變數名稱,時常會重覆地使用。這項特性就會造成潛在的 bug。

在 ES6 之後,引入 let 保留字,修掉了這項特性。將上例改寫如下:

for (let i = 0; i < 10; i++) { /* Pass. */ }

assert(typeof i === "undefined", "i should not exist");

// Home-made `assert`
function assert (cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

這時候 i 是未定義的,符合程式人對主流程式語言的期待。

同樣地,while 迴圈也沒有區塊範疇,所以下例可「正確」執行:

while (true) {
    var n = 10;

    break;
}

assert(n === 10, "n should be 10");

// Home-made `assert`
function assert (cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

在 ES6 後引入 let,修掉了這項特性。將上例改寫如下:

while (true) {
    let n = 10;

    break;
}

assert(typeof n === "undefined", "n should not exist");

// Home-made `assert`
function assert (cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

JavaScript 的函式範疇 (Function Scope)

相對來說,JavaScript 的函式區塊內視為新的範疇,內部的變數不會溢到函式外。參考下例:

function doSomething () {
    while (true) {
        var n = 10;

        break;
    }
}

doSomething();

assert(typeof n === "undefined", "n should not exist");

// Home-made `assert`
function assert (cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

由於現在已經有 let 保留字了,如果只是為了保持命名空間的乾淨,這樣動作便顯得多餘。

此外,這裡還是會在全域空間中引入一個額外的函式名稱。由於 JavaScript 可撰寫匿名函式 (anonymous function),在 JavaScript 有更好的方式,詳見下一節的敘述。

以 IIFE 立即執行 JavaScript 程式

IIFE (Immediately Invoked Function Expression) 是一種 JavaScript 的經典模式。其原理就是撰寫匿名函式後立即執行該函式。由於函式具有函式範疇,使用 IIFE 撰寫的 JavaScript 程式不會在命名空間引入新的變數,可以保持命名空間不受汙染。

以下範例展示典型的 IIFE 寫法:

// Classic IIFE.
(function () {
    while (true) {
        var n = 10;

        break;
    }
})();

assert(typeof n === "undefined", "n should not exist");

// Home-made `assert`
function assert (cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

典型的 IIFE 模式用一對小括號包住匿名函式,後面再放一對小括號。這樣的寫法表示立即執行該匿名函式。

另一種變體是在匿名函式前加上驚嘆號:

// IIFE variant.
!function () {
    while (true) {
        var n = 10;

        break;
    }
}();

assert(typeof n === "undefined", "n should not exist");

// Home-made `assert`
function assert (cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

還有一種變體是在匿名函式前寫上 void

// IIFE variant.
void function () {
    while (true) {
        var n = 10;

        break;
    }
}();

assert(typeof n === "undefined", "n should not exist");

// Home-made `assert`
function assert (cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

一般來說,最好寫成典型的 IIFE。另外兩種寫法只要能讀得懂即可。

使用 IIFE 的小技巧

寫 IIFE 本質上是在寫匿名函式,我們可以將參數傳入該匿名函式中。根據這個概念,衍生出數個撰寫 IIFE 中常見的小技巧。

我們可以把參數傳入 IIFE 的匿名函式中,像是以下例子傳入瀏覽器的 window 物件

(function (global) {
    global.a = 3;
})(window);

assert(a === 3, "a should be 3");

// Home-made `assert`
function assert(cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

在這個例子中,globalwindow 是等義的。附在 global 上的變數即為全域變數。

以下例子用 IIFE 來保護 undefined

(function (undefined) {
    let a;

    assert(a === undefined, "a should be undefined");
})();

// Home-made `assert`
function assert(cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

在這個例子中,我們將 undefined 設為參數,但卻刻意不傳入值。如此一來,在 IIFE 的匿名函式中,可確保 undefied 沒有受到外部程式的汙染,保持在未定義的狀態。

綜合以上兩個實例的概念就可以寫成以下代碼:

(function (window, undefined) {
    // Run code here.
})(window);

讀者應該可以了解在這個 IIFE 代碼中,windowundefined 分別表示的意義。

我們最後來看一個利用 IIFE 形成依賴注入 (dependency injection) 的實例:

(function (fn) {
    fn(window);
})(function (global) {
    global.a = 3;
});

assert(a === 3, "a should be 3");

// Home-made `assert`
function assert(cond, msg) {
    if (!(cond)) {
        throw msg;
    }
}

在我們這個例子中,共有兩個匿名函式。第一個匿名函式是原本要執行的函式,但我們把實際要執行的函式以參數的形式傳入,這時候該函式實際的行為會由外部參數來決定。第二個匿名函式做為參數傳入第一個匿名函式,並決定了這段程式碼實際的行為。這個模式在 AMDUMD 即可見到。

最後這個例子稍微難一點,因為這用到了函數式程式設計的概念。

結語

在本文中,我們看到 JavaScript 原本的範疇議題及後來的修正方式。此外,也學會了 IIFE 模式。透過這些手法,範疇應該不再是問題了。

關於作者

身為資訊領域碩士,美思認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

美思喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,美思將所學寫成文章,放在這個網站上和大家分享。