前言
在 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
卻保留了下來。像是 i
、j
這類計數器常用的變數名稱,時常會重覆地使用。這項特性就會造成潛在的 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;
}
}
在這個例子中,global
和 window
是等義的。附在 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 代碼中,window
和 undefined
分別表示的意義。
我們最後來看一個利用 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;
}
}
在我們這個例子中,共有兩個匿名函式。第一個匿名函式是原本要執行的函式,但我們把實際要執行的函式以參數的形式傳入,這時候該函式實際的行為會由外部參數來決定。第二個匿名函式做為參數傳入第一個匿名函式,並決定了這段程式碼實際的行為。這個模式在 AMD 或 UMD 即可見到。
最後這個例子稍微難一點,因為這用到了函數式程式設計的概念。
結語
在本文中,我們看到 JavaScript 原本的範疇議題及後來的修正方式。此外,也學會了 IIFE 模式。透過這些手法,範疇應該不再是問題了。