1. 程式人生 > >閉包和類

閉包和類

閉包

先上維基百科的定義

在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures),是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函式和與其相關的引用環境組合而成的實體。閉包在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。

簡單理解這句話,有兩個要點:
1. 自由變數 2. (引用自由變數的)函式。

先說自由變數:

當我們定義一個變數時,如果不對它指定約束條件,它就是自由變數。

 舉個例子:

x ∈  (0,99)
f(x,y)

在函式f(x,y)中,x就是約束變數,y是自由變數。

具體到JavaScript中,看一個例子:

var x = 0;
function foo (y) {
    var z = 2;
    return x + y + z;
}
foo (3); // 3

轉換成數學思維的話,函式foo其實應該是這樣的foo(x,y),但是我們知道函式的引數其實是受到函式的約束的,也就是說,真正的自由變數只有x一個。
這樣可以引出一個簡單的定義,在函式中,如果存在一個既不是區域性變數,也不是形參的變數,我們可以認為形成了閉包。

自由變數從哪兒來?

幾乎所有的語言中,對於同名變數都是就近尋找,先在本作用域內尋找,找不到就去父作用域找。我們稱之為作用域鏈。
在一個閉包函式中,自由變數通常是由父級提供。看下面的例子:

function foo(x) {
    var tmp = 3;
    function bar(y) {
        console.log(x + y + (++tmp));
    }
    bar(10);
}
foo(2)

根據我們上面的定義,bar擁有自由變數,是閉包,而foo不是。
那麼怎樣才能讓foo變成閉包呢?

var x = 0;
function foo() {
    var tmp = 3;
    function bar(y) {
        console.log(x + y + (++tmp));
    }
    bar(10);
}
// 其實轉換一下,形如
function foo2() {
    var tmp = 3;
    //function bar(y) {
        console.log(x + 10 + (++tmp));
    //}
    // bar(10);
}

此時,可以認為foo是一個閉包。
到這裡,可能有朋友覺得這和平時看到的js閉包不一樣啊,我們平時看到的閉包,都是這樣的:例子來自這篇部落格

function foo(x) {
    var tmp = new Number(3);
    return function (y) {
        alert(x + y + (++tmp));
    }
}
var bar = foo(2); // bar 現在是一個閉包
bar(10);

這個函式其實可以改寫成下面的樣子:

bar = function (y) {
    // foo(2)
    alert(2 + y + (++tmp))
}

很明顯,tmp是自由變數,符合我們起初的定義,bar是擁有自由變數的函式。
那麼tmp存在哪兒呢?
在執行foo(2)時,就會產生一個tmp=3的變數。這個變數被return的函式所引用,所以不會被回收。而return的函式中的自由變數,根據作用域鏈去尋找值。bar函式,是在foo(2)中定義的,所以,變數tmp先在foo(2)的變數區中去尋找,並對其操作。

注:有關作用域鏈的問題,我會在下一篇做解析。

說到這裡,插一下module模式

閉包使用之module模式

var Module = (function () {
    var aaa = 0;
    var foo = function () {
        console.log(aaa);
    }
    
    return {
        Foo: foo
    }
})();
// 或者
(function () {
    var aaa = 0;
    var foo = function () {
        console.log(aaa);
    }
    
    window.Module = {
        Foo: foo
    }
})();

注意上面的兩個例子,Module本身只是一個物件,但是return的函式本身形成了閉包,保證了作用域的乾淨,不會汙染到其他函式。

說到這裡,想必有朋友覺得這不就是個另類的類嗎?擁有區域性變數,還有可訪問的函式。沒錯,就外現而言,我認為閉包和類是非常相似的。

以Java舉例:

class Foo {
    private int a;
    int Say( int b ) {
        return a + b; 
    }  
}

上面的Foo中,函式Say中的a是函式作用域外的,屬於自由變數。可以認為Say形成了函式閉包。但是與js不同的地方就在於,例項方法需要通過類的例項也就是物件來呼叫。
在java的設計裡,明確了訪問許可權,private,protect,default,package,這是規範呼叫的創舉。這也使得java程式設計師很少會考慮閉包這種實現,因為變數和函式都有關鍵字來定義訪問許可權,歸屬於一個個類中,明確且清晰。

閉包的壞處

如果把閉包按照類的實現來理解的話,很容易就明白為什麼不建議使用閉包。
每次呼叫閉包,就會生成一個作用域來存放一些閉包函式需要的自由變數。這很容易造成記憶體浪費。即使在Java程式設計中,也不建議隨便就新建物件。

題外話

在前一篇bind、call、apply中,我提到了一個觀點,因為是面向物件,所以存在繫結this的需要。
關於面向物件,我認為,面向物件的好處就在於,易於理解,方便維護和複用。這在多人開發大型專案時,是遠遠超過對效能的要求的。
即使在摩爾定律放緩的現在,相對於以前,記憶體也是非常便宜的,所以從遠古時代對於效能要求到極致,到現在普遍提倡程式碼可讀性。
有超級大牛建立了這個繽紛的程式碼世界,為了讓更多人體會到程式設計的樂趣,他們設計了更易理解的程式語言,發明了各種編譯器、解析器……
如果只是寫一個1+1的程式,是不需要面向物件的,如果人類能和機器擁有超強的邏輯和記憶,也是不需要面向物件的。