[譯]其實閉包並不高深莫測

其實閉包並不高深莫測

標籤: JavaScript closure translategit


本文由 伯樂在線 - 劉健超-J.c 翻譯,Namco 校稿。未經許可,禁止轉載!github

英文出處:Igor Šarčevićweb

幾年前,我仍是一名高中生時,個人一個朋友向我講述了閉包的概念。雖然我當時一點也不明白他想表達的內容,但他在向我講述時卻表現得很是高大上。對於當時的我來講,閉包看來是一個深不可測的魔法。即便 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

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

引入咱們第一個閉包

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

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

儘管 display()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 建立「真正的」隱藏域,或者須要建立簡單的構造器時,閉包這個屬性是很是好用的。不過對於通常的類來講,閉包可能仍是有點過重了。

感謝您的閱讀。 若是你以爲這篇文章對您有幫助或者以爲我翻譯得不錯,那給我個 star 吧。

相關文章
相關標籤/搜索