原文連接:http://web.jobbole.com/84456/前端
原文推薦的前端書籍的連接:http://web.jobbole.com/86734/web
咱們將從一個簡單的問題開始。若是將閉包引入到該程序中,將能輕易解決這個問題。編程
咱們爲計數事件建立一個機制。該機制將有助於咱們跟蹤代碼的執行,甚至去調試一些問題。例如,我會如下面的方式調用計數器:閉包
JavaScript編程語言
1函數式編程 2函數 3this |
increment(); // Number of events: 1spa increment(); // Number of events: 2調試 increment(); // Number of events: 3 |
正如你所看到的上述案例,咱們但願代碼會在咱們每次執行 increment() 函數時,會顯示一條信息「Number of events: x」。下面以簡單的方式實現該函數:
JavaScript
1 2 3 4 5 6 7 |
var counter = 0;
function increment() { counter = counter + 1;
console.log("Number of events: " + counter); } |
上述代碼很是簡單明確。然而,當咱們引入第二個計數器時,就會很快遇到問題。固然,咱們能實現兩個單獨的計數器機制,以下面的代碼,但很明顯有須要改進的地方:
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 |
上述代碼出現了沒必要要的重複。明顯地,這種解決辦法並不適用於超過二或三個記數器的狀況。咱們須要想出更好的解決方案。
在保持與上述例子類似的狀況下,咱們以某種方式引入新的計數器,該計數器捆綁了一個能自增的函數,並且沒有大量重複的代碼。下面嘗試使用閉包:
JavaScript
1 2 3 4 5 6 7 8 9 10 11 |
function createCounter() { var counter = 0;
function increment() { counter = counter + 1;
console.log("Number of events: " + counter); }
return increment; } |
讓咱們看看這是如何工做的。咱們將建立兩個計數器,並讓它們跟蹤兩個獨立的事件:
JavaScript
1 2 3 4 5 6 7 8 9 |
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。請注意,這並非返回調用函數的運行結果,而是函數自己。
這就意味着,當咱們在這個代碼段下面建立新的計數器時,其實是生成新函數。
JavaScript
1 2 3 4 |
// fancyNewCounter is a function in this scope // fancyNewCounter 是當前做用域的一個函數
var fancyNewCounter = createCounter(); |
這就是閉包生命週期的力量所在。每一個生成的函數,都會保持在 createCounter() 所建立的 counter 變量的引用。在某種意義上,被返回的函數記住了它所被建立時的環境。
在這裏須要提醒你們注意的是,內部變量 counter 都是獨立存在於每一個做用域!例如,若是咱們建立兩個計數器,那麼它們都會在閉包體內會分配一個新的 counter 變量。咱們觀察如下代碼:
每一個計數器都會從 1 算起:
JavaScript
1 2 3 4 5 6 |
var counter1 = createCounter(); counter1(); // Number of events: 1 counter1(); // Number of events: 2
var counter2 = createCounter(); counter2(); // Number of events: 1 |
第二個計數器並不會干擾第一個計數器的值:
JavaScript
1 |
counter1(); // Number of events: 3 |
信息「Number of events: x」 是沒問題的,但若是能描述每一個計數事件的類型,那麼這將會更好。如如下例子,咱們爲計數器添加名字:
JavaScript
1 2 3 4 5 6 |
var catCounter = createCounter("cats"); var dogCounter = createCounter("dogs");
catCounter(); // Number of cats: 1 catCounter(); // Number of cats: 2 dogCounter(); // Number of dogs: 1 |
咱們僅需經過爲閉包傳遞參數就能達到這種目的。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 |
function createCounter(counterName) { var counter = 0;
function increment() { counter = counter + 1;
console.log("Number of " + counterName + ": " + counter); }
return increment; } |
很是棒!請注意上述 createCounter() 函數的一個有趣行爲。返回函數不只記住了局部變量 counter,並且記住了傳遞進來的參數。
我所說的公用接口是指,咱們如何使用計數器。這並不單純指,當被建立的計數器被調用時會增長值。
JavaScript
1 2 3 |
var dogCounter = createCounter("dogs");
dogCounter.increment(); // Number of dogs: 1 |
讓咱們建立這樣的一個實現:
JavaScript
1 2 3 4 5 6 7 8 9 10 11 |
function createCounter(counterName) { var counter = 0;
function increment() { counter = counter + 1;
console.log("Number of " + counterName + ": " + counter); };
return { increment : increment }; } |
在上述代碼段,咱們簡單地返回一個對象,該對象包含了該閉包的全部功能。在某種意義下,咱們能定義閉包能返回的一系列信息。
如今,咱們能很是簡單地爲咱們的計數器引入減量(decrement)。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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 函數。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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() 函數看起來很是類似,然而這是截然不同的。咱們沒有在結果對象返回計數值!這意味着如下代碼將會調用失敗:
JavaScript
1 2 3 |
var dogsCounter = createCounter("dogs");
dogsCounter.display(); // ERROR !!! |
咱們讓 display() 函數對外部來講是不可見的。它僅在 createCounter() 內可用。
正如你所見,咱們經過閉包能很是簡單地引入抽象數據類型。例如,讓咱們經過閉包實現一個 堆棧。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 建立「真正的」隱藏域,或者須要建立簡單的構造器時,閉包這個屬性是很是好用的。不過對於通常的類來講,閉包可能仍是有點過重了。