ES6 學習筆記之二 塊做用域與閉包

「閉包是函數和聲明該函數的詞法環境的組合。」chrome

這是MDN上對閉包的定義。閉包

《JavaScript高級程序設計》中則是這樣定義的:閉包是指有權訪問另外一個函數做用域中的變量的函數。函數

我的更傾向於MDN的閉包定義,緣由有三:測試

其一,若是僅將閉包定義爲可訪問其父做用域(鏈)的局部變量的函數,那麼就忽視了它持有外部環境(使外部做用域不被銷燬)的意義。spa

其二,閉包有權訪問的必然是其父做用域(鏈)中的局部變量,「另外一個函數做用域」的說法不夠明確清晰。firefox

其三,就是本篇博文的主題了,閉包在ES6中,是不限於訪問另外一個函數的做用域的,還能夠是塊做用域。固然,《JavaScript高級程序設計》這本書出版時,尚未ES6,書裏也明確說明JavaScript是沒有塊做用域的,所以這一點不能成爲批評《JavaScript高級程序設計》的理由。設計

定義一般講究嚴謹、言簡意賅,也就意味着不太好理解。code

換個通俗點的說法,閉包就是指在一個非全局做用域中聲明的函數及其所在的這個做用域,這個函數在該做用域外被調用時,仍然可以訪問到該做用域內的(局部)變量。若是不在聲明函數的做用域外調用,或者該函數沒有訪問外部做用域的局部變量,閉包也就沒有什麼存在的意義了。blog

因爲ES6出現之前,沒有塊做用域,這個非全局做用域就只能是一個函數了,那麼閉包就是聲明在另外一個函數內部的函數(及其所在的函數)了。爲了實如今聲明它的做用域外也能調用該函數,就須要將該函數做爲一個返回值,返回到父做用域(父級函數)以外了。ip

舉例說明(例1):

var age = 30;
var fn;
fn = (function () {
    var age = 20;
    var name = "Tom";
    return function () {
        console.log("name is " + name + ".");
        console.log("age is " + age + ".");
    };
})();
fn();

運行結果:

name is Tom.
age is 20.

能夠看到,age 獲取的是匿名函數中聲明的局部變量 age 的值 20,不是全局變量 age 的值 30。name 更是乾脆沒有同名全局變量,只有匿名函數中聲明的局部變量。

對於ES6,由於塊做用域的存在,閉包就有了另外一種實現,舉例以下(例2) 

let age = 30;
let fn;
{
    let age = 20;
    let name = "Tom";
    fn = function () {
        console.log("name is " + name + ".");
        console.log("age is " + age + ".");
    };
}
fn();

運行結果與例1相同:

name is Tom.
age is 20.

可見,在ES6中,聲明在塊做用域內的函數,在離開塊做用域後,優先訪問的依然是聲明它的塊做用域的局部變量。

在《你不知道的JavaScript》中文版下卷中,曾經提到過塊做用域函數,即聲明在塊做用域內的函數,在塊外沒法調用。

原文的例子以下(例3):

{
    foo();
    function foo() {
        //...
    }
}
foo();

書中認爲,第一個foo()調用會正常返回結果,第二個foo()調用會報 ReferenceError 錯誤。經在 chrome(64.0) 和 firefox(58.0)版中測試,非嚴格模式下,兩個調用均正常返回結果,不會出現 ReferenceError 錯誤。僅在嚴格模式下,與其預期結果相同。

也就是說,非嚴格模式下,聲明在塊做用域內的函數,是在塊做用域外的父做用域中有效的。

這也致使了例2還有一個變體(例4):

let age = 30;
{
    let age = 20;
    let name = "Tom";
    function fn() {
        console.log("name is " + name + ".");
        console.log("age is "+ age + ".");
    }
}
fn();

其結果也是:

name is Tom.
age is 20.

究其緣由,在於函數是在塊做用域內聲明的,所以它在被調用時,會優先訪問塊做用域內的局部變量。又由於它雖然是在塊內聲明,卻被提高至其父做用域,因此能夠在塊做用域外被訪問。

不過這種寫法,意圖不夠清晰,且在多層做用域的狀況下,容易產生混亂,嚴格模式下,還會致使錯誤。

 

如今再來看上一篇博文中的循環變量的例子(例1七、例18和例19):

(例17)

var i;
var fn = [];
for (i = 0; i < 3; i++) {
    fn.push(function () {
        console.log(i);
    });
}
fn[0]();
fn[1]();
fn[2]();

之因此會輸出三個3,是由於函數在調用時纔會嘗試獲取i值,而不是在定義時就獲取了i的值,而調用是在循環以後發生的。調用時由於i是全局變量,其值已經在循環中自增到了3。所以3次調用均返回3。

(例19)

var i;
var fn  = [];
for (i = 0; i < 3; i++) {
    fn.push((function (i) {
        return function () {
            console.log(i);
        }
    })(i));
}
fn[0]();
fn[1]();
fn[2]();

實際是個障眼法,循環內部的函數定義中,形參使用了和全局變量 i 同名的變量,因爲子做用域同名變量的遮蔽做用,函數內部的 i 實際已經不是全局變量 i 了,而是一個匿名函數內部的局部變量。調用匿名函數時,將全局變量 i 的值傳遞給了局部變量 i 。而返回的那個閉包函數,按照閉包的定義,不管在何處調用,都只會先訪問其父做用域中的局部變量。

若是把匿名函數中的 i 換個名字,就更能清晰地看出閉包在這裏的做用了:

var i;
var fn  = [];
for (i = 0; i < 3; i++) {
    fn.push((function (k) {
        return function () {
            console.log(k);
        }
    })(i));
}
fn[0]();
fn[1]();
fn[2]();

而(例18):

var fn = [];
for (let i = 0; i < 3; i++) {
    fn.push(function () {
        console.log(i);
    });
}
fn[0]();
fn[1]();
fn[2]();

就恰好是本篇博文所說的塊做用域閉包。每一個循環都會產生一個塊做用域;而 for 語句中的 let,會在每一個循環產生的塊做用域內生成一個局部變量 i;聲明在每一個循環內的匿名函數,都會優先訪問聲明本身的那個循環產生的塊做用域中的 i 的值。

其實際意義與以下例子是同樣的:

var fn = [];
for (let i = 0; i < 3; i++) {
    let k = i;
    fn.push(function () {
        console.log(k);
    });
}
fn[0]();
fn[1]();
fn[2]();

比較而言,用函數做爲外部做用域的閉包,能夠用返回閉包函數的方式將閉包函數傳遞到閉包做用域外。而塊做用域閉包沒辦法使用return,就只能是直接爲外部做爲域的變量賦值的方式,將閉包函數傳遞出去。

不過,對於例19,能夠改形成不使用返回值,直接在閉包函數內使用外部做用域變量的形式:

var i;
var fn  = [];
for (i = 0; i < 3; i++) {
    (function (k) {
        fn.push(function () {
            console.log(k);
        });
    })(i));
}
fn[0]();
fn[1]();
fn[2]();

因爲這種匿名函數當即調用的方式構造的閉包只執行一次,要將閉包函數傳遞給哪一個變量,也是coding時可以肯定的,返回值傳遞,仍是直接使用外部變量,都是同樣的。而這種形式,在ES6中均可以用塊做用域閉包代替。

就代碼自己的理解難度而言,ES6的塊級做用域更容易一些。

回到本文開頭的閉包定義,廣義的解讀,因爲任何一個函數必然有聲明它的記法環境,因此全部的函數和聲明它的記法環境都構成閉包。好比全局做用域內的函數,它和全局做用域就構成了閉包。這也是《ES6標準入門》(阮一峯)在「let 和 const」一章中解釋例17時,會說fn[*]()的調用是經過閉包獲取的全局變量 i 的緣由吧。

PS:

順便說一下塊做用域函數,《你不知道的JavaScript》中,關於塊做用域函數有兩個示例,其一見上文例3。

另外一個例子以下(例4):

if (something) {
    function foo() {
        console.log("1");
    }
} else {
    function foo() {
        console.log("2");
    }
}
foo();

原文說,在前ES6環境中(應該至關於非嚴格模式),無論something的值是什麼,foo()都會打印出「2」,由於兩個函數聲明都被提高到了塊外,第二個總會勝出。

經在chrome(64.0) 和 firefox(58.0)版中測試,實際運行結果是:something爲真,foo()打印「1」,something爲假,foo()打印「2」。

嚴格模式下,則與其預期相符,拋出一個ReferenceError。

其實這種全局定義函數,在ES6中,與函數變量方式相比,不能算是最佳實踐了。

相關文章
相關標籤/搜索