其實閉包並不高深莫測

幾年前,我還是一名高中生時,我的一個朋友向我講述了閉包的概念。雖然我當時一點也不明白他想表達的內容,但他在向我講述時卻表現得非常高大上。對於當時的我來說,閉包看來是一個深不可測的魔法。即使 Google 後也不能解除我的疑惑。而所有我能查閱的科技文章,都爲高中生所難以理解。


現在的我回想起高中編程時光,我都會不禁一笑。這是一篇試圖用一些簡單項目去解釋閉包的文章,這會幫助我的學弟學妹們能輕易地駕馭強大的閉包。


計數事件


我們將從一個簡單的問題開始。如果將閉包引入到該程序中,將能輕易解決這個問題。


我們爲計數事件創建一個機制。該機制將有助於我們跟蹤代碼的執行,甚至去調試一些問題。例如,我會以下面的方式調用計數器:


increment(); // Number of events: 1

increment(); // Number of events: 2

increment(); // Number of events: 3


正如你所看到的上述案例,我們希望代碼會在我們每次執行 increment() 函數時,會顯示一條信息“Number of events: x”。下面以簡單的方式實現該函數:


var counter = 0;

function increment() {

counter = counter + 1;

console.log("Number of events: " + counter);

}


多個計數器


上述代碼非常簡單明確。然而,當我們引入第二個計數器時,就會很快遇到問題。當然,我們能實現兩個單獨的計數器機制,如下面的代碼,但很明顯有需要改進的地方:


var counter1 = 0;

function incrementCounter1() {

counter1 = counter1 + 1;

console.log("Number of events: " + counter1);

}

var counter2 = 0;

function incrementCounter2() {

counter2 = counter2 + 1;

console.log("Number of events: " + counter2);

}

incrementCounter1(); // Number of events: 1

incrementCounter2(); // Number of events: 1

incrementCounter1(); // Number of events: 2


上述代碼出現了不必要的重複。明顯地,這種解決辦法並不適用於超過二或三個記數器的情況。我們需要想出更好的解決方案。


引入我們第一個閉包


在保持與上述例子相似的情況下,我們以某種方式引入新的計數器,該計數器捆綁了一個能自增的函數,而且沒有大量重複的代碼。下面嘗試使用閉包:


function createCounter() {

var counter = 0;

function increment() {

counter = counter + 1;

console.log("Number of events: " + counter);

}

return increment;

}


讓我們看看這是如何工作的。我們將創建兩個計數器,並讓它們跟蹤兩個獨立的事件:


var counter1 = createCounter();

var counter2 = createCounter();

counter1(); // Number of events: 1

counter1(); // Number of events: 2

counter2(); // Number of events: 1

counter1(); // Number of events: 3


啊,這看起來有點複雜…然而,這實際上是非常簡單的。我們只需將實現邏輯分成幾個易於理解的塊。下面就看看我們實現了什麼:


首先,創建了一個名爲 counter 的局部變量。


然後,創建了一個名爲 increment 的局部函數,它能增加 counter 變量值。如果你從未接觸過將函數作爲數據來處理的函數式編程,這也許對你非常陌生。然而,這是非常常見的,而且只需要一些練習就能適應這一概念。

你應該注意到這一點,createCounter() 的實現與我們原先的計數器實現幾乎一致。唯一不同的是它被包裝或封裝在一個函數體內。因此,這些構造器都被稱爲閉包。


現在是棘手的部分:


在 createCounter() 的最後一步返回了局部函數 increment。請注意,這並不是返回調用函數的運行結果,而是函數本身。

這就意味着,當我們在這個代碼段下面創建新的計數器時,實際上是生成新函數。


// fancyNewCounter is a function in this scope

// fancyNewCounter 是當前作用域的一個函數

var fancyNewCounter = createCounter();


這就是閉包生命週期的力量所在。每個生成的函數,都會保持在 createCounter() 所創建的 counter 變量的引用。在某種意義上,被返回的函數記住了它所被創建時的環境。


在這裏需要提醒大家注意的是,內部變量 counter 都是獨立存在於每個作用域!例如,如果我們創建兩個計數器,那麼它們都會在閉包體內會分配一個新的 counter 變量。我們觀察以下代碼:


每個計數器都會從 1 算起:


var counter1 = createCounter();

counter1(); // Number of events: 1

counter1(); // Number of events: 2

var counter2 = createCounter();

counter2(); // Number of events: 1


第二個計數器並不會干擾第一個計數器的值:


counter1(); // Number of events: 3


爲我們的計數器命名


信息“Number of events: x” 是沒問題的,但如果能描述每個計數事件的類型,那麼這將會更好。如以下例子,我們爲計數器添加名字:


var catCounter = createCounter("cats");

var dogCounter = createCounter("dogs");

catCounter(); // Number of cats: 1

catCounter(); // Number of cats: 2

dogCounter(); // Number of dogs: 1


我們僅需通過爲閉包傳遞參數就能達到這種目的。


function createCounter(counterName) {

var counter = 0;

function increment() {

counter = counter + 1;

console.log("Number of " + counterName + ": " + counter);

}

return increment;

}


非常棒!請注意上述 createCounter() 函數的一個有趣行爲。返回函數不僅記住了局部變量 counter,而且記住了傳遞進來的參數。


改善公用接口


我所說的公用接口是指,我們如何使用計數器。這並不單純指,當被創建的計數器被調用時會增加值。


var dogCounter = createCounter("dogs");

dogCounter.increment(); // Number of dogs: 1


讓我們創建這樣的一個實現:


function createCounter(counterName) {

var counter = 0;

function increment() {

counter = counter + 1;

console.log("Number of " + counterName + ": " + counter);

};

return { increment : increment };

}


在上述代碼段,我們簡單地返回一個對象,該對象包含了該閉包的所有功能。在某種意義下,我們能定義閉包能返回的一系列信息。


增加一個減量


現在,我們能非常簡單地爲我們的計數器引入減量(decrement)。


function createCounter(counterName) {

var counter = 0;

function increment() {

counter = counter + 1;

console.log("Number of " + counterName + ": " + counter);

};

function decrement() {

counter = counter - 1;

console.log("Number of " + counterName + ": " + counter);

};

return {

increment : increment,

decrement : decrement

};

}

var dogsCounter = createCounter("dogs");

dogsCounter.increment(); // Number of dogs: 1

dogsCounter.increment(); // Number of dogs: 2

dogsCounter.decrement(); // Number of dogs: 1


隱藏計數器行爲


上述代碼有兩處冗餘的代碼行。沒錯,就是 console.log。如果能創建一個專門用於顯示計數器值的函數將會更好。讓我們調用 display 函數。


function createCounter(counterName) {

var counter = 0;

function display() {

console.log("Number of " + counterName + ": " + counter);

}

function increment() {

counter = counter + 1;

display();

};

function decrement() {

counter = counter - 1;

display();

};

return {

increment : increment,

decrement : decrement

};

}

var dogsCounter = createCounter("dogs");

dogsCounter.increment(); // Number of dogs: 1

dogsCounter.increment(); // Number of dogs: 2

dogsCounter.decrement(); // Number of dogs: 1


increment() 和 decrement() 函數看起來非常相似,然而這是大相徑庭的。我們沒有在結果對象返回計數值!這意味着以下代碼將會調用失敗:


var dogsCounter = createCounter("dogs");

dogsCounter.display(); // ERROR !!!


我們讓 display() 函數對外部來說是不可見的。它僅在 createCounter() 內可用。


抽象數據類型


正如你所見,我們通過閉包能非常簡單地引入抽象數據類型。例如,讓我們通過閉包實現一個 堆棧。


function createStack() {

var elements = [];

return {

push: function(el) { elements.unshift(el); },

pop: function() { return elements.shift(); }

};

}

var stack = createStack();

stack.push(3);

stack.push(4);

stack.pop(); // 4


注意:在 JavaScript 中,閉包並不是堆棧數據類型的最佳實現方式。用 Prototype 實現會對內存更友好(譯者注:在當前對象實例找不會相應屬性或方法時,會到相應實例共同引用的 Prototype 屬性尋找相應屬性或方法(如果在當前Prototype屬性找不到時,會沿着當前原型鏈向上查找),而Prototype 上的屬性或方法是公用的,而不像實例的屬性或方法那樣,各自單獨創建屬性或方法,從而節省更多的內存)。


閉包與面向對象編程


如果你具有 面向對象編程 的經歷,那麼你應該會注意到上述構造器看來非常像類、對象、實例值和私有/公有方法。


閉包與類相似,都會將一些能操作內部數據的函數聯繫在一起。因此,你能在任何地方像使用對象一樣使用閉包。


結語


閉包是編程語言一個很棒的屬性。當我們想在 JavaScript 創建“真正的”隱藏域,或者需要創建簡單的構造器時,閉包這個屬性是非常好用的。不過對於一般的類來說,閉包可能還是有點太重了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章