據說你還不理解JavaScript閉包

閉包(Closure)

閉包是一個函數和詞法環境的組合,函數聲明在這個詞法環境中

詞法做用域

看下面一個例子css

function init() {
  var name = 'Mozilla'; // name是局部變量
  function displayName() { // displayName()是內部函數,一個閉包
    alert(name); // 使用外部函數聲明的變量
  }
  displayName();
}
init();

  init()建立了一個局部變量name和一個函數displayName()。函數displayName()是一個已經定義在init()內部的函數,而且只能在函數init()裏面才能訪問獲得。函數displayName()沒有本身的局部變量,但因爲內部函數能夠訪問外部函數變量,displayName()能夠訪問到聲明在外部函數init()的變量name,若是局部變量還存在的話,displayName()也能夠訪問他們。html

閉包

看下面一個例子前端

function makeFunc() {
  var name = 'Mozilla';
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

  運行這段代碼你會發現和以前init()的方法是同樣的效果,但不一樣之處是,displayName()在執行以前,這個內部方法是從外部方法返回來的。
  首先,代碼仍是會正確運行,在一些編程語言當中,一個函數內的局部變量只存在於該函數的執行期間,隨後會被銷燬,一旦makeFunc()函數執行完畢的話,變量名就不可以被獲取,可是,因爲代碼仍然正常執行,這顯然在JS裏是不會這樣的。這是由於函數在JS裏是以閉包的形式出現的,閉包是一個函數和詞法做環境的組合,詞法環境是函數被聲明的那個做用域,這個執行環境包括了建立閉包時同一建立的任意變量,即建立的這個函數和這些變量處於同一個做用域當中。在這個例子當中,myFunc()是displayName()的函數實例,makeFunc建立的時候,displayName隨之也建立了。displayName的實例能夠得到詞法做用域的引用,在這個詞法做用域當中,存在變量name,對於這一點,當myFunc調用的話,變量name,仍然能夠被調用,所以,變量'Mozilla'傳遞給了alert函數。編程

這裏還有一個例子 - 一個makeAdder函數數組

function makeAdder (x) {
  return function(y) {
    return x + y;
  }
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

  在這個例子當中,咱們定義了一個函數makeAdder(x),傳遞一個參數x,而且返回一個函數,這個返回函數接收一個參數y,並返回x和y的和。
  實際上,makeAdder是一個工廠模式 - 它建立了一個函數,這個函數能夠計算特定值的和。在上面這個例子當中,咱們使用工廠模式來建立新的函數 - 一個與5進行加法運算,一個與10進行加法運算。add5和add10都是閉包,他們共享相同的函數定義,但卻存儲着不一樣的詞法環境,在add5的詞法環境當中,x爲5;在add10的詞法環境當中,x變成了10。閉包

閉包的實踐

  閉包是頗有用的,由於他讓你把一些數據(詞法環境)和一些可以獲取這些數據的函數聯繫起來,這有點和麪向對象編程相似,在面向對象編程當中,對象讓咱們能夠把一些數據(對象的屬性)和一個或多個方法聯繫起來
  所以,你可以像對象的方法同樣隨時使用閉包。實際上,大多數的前端JS代碼都是事件驅動性的 - 咱們定義一些事件,當這個事件被用戶所觸發的時候(例如用戶的點擊事件和鍵盤事件),咱們的事件一般會帶上一個回調:即事件觸發所執行的函數。例如,假設咱們但願在頁面上添加一些按鈕,這些按鈕可以調整文字的大小,實現這個功能的方式是肯定body的字體大小,而後再設置頁面上其餘元素(例如標題)的字體大小,咱們使用em做爲單位。app

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

  咱們設置的調節字體大小的按鈕可以改變body的font-size,而且這個調節可以經過相對字體單位,反應到其餘元素上,編程語言

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

  size12,size14,size16是三個分別把字體大小調整爲12,14,16的函數,咱們能夠把他們綁定在按鈕上。函數

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>

經過閉包來封裝私有方法

  相似JAVA語言可以聲明私有方法,意味着只可以在相同的類裏面被調用,JS沒法作到這一點,但卻能夠經過閉包來封裝私有方法。私有方法不限制代碼:他們提供了管理命名空間的一種強有力方式。
  下面代碼闡述了怎樣使用閉包來定義公有函數,公有函數可以訪問私有方法和屬性。性能

var counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };   
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

  在先前的例子當中,每一個閉包具備他們本身的詞法環境,在這個例子中,咱們建立了一個單獨的詞法環境,這個詞法環境被3個函數所共享,這三個函數是counter.increment, counter.decrement和counter.value
  共享的詞法環境是由匿名函數建立的,必定義就能夠被執行,詞法環境包含兩項:變量privateCounter和函數changeBy,這些私有方法和屬性不可以被外面訪問到,然而,他們可以被返回的公共函數訪問到。這三個公有函數就是閉包,共享相同的環境,JS的詞法做用域的好處就是他們能夠互相訪問變量privateCounter和changeBy函數

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var counter1 = makeCounter();
var counter2 = makeCounter();
alert(counter1.value()); /* Alerts 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* Alerts 2 */
counter1.decrement();
alert(counter1.value()); /* Alerts 1 */
alert(counter2.value()); /* Alerts 0 */

  兩個計數器counter1和counter2分別是互相獨立的,每一個閉包具備不一樣版本的privateCounter,每次計數器被調用,詞法環境會改變變量的值,可是一個閉包裏變量值的改變並不影響另外一個閉包裏的變量。

循環中建立閉包:常見錯誤

下面一個例子

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

  helpText 數組定義了三個有用的hint,每一個分別與輸入框的id相對應,每一個方法與onfocus事件綁定起來。當你運行這段代碼的時候,不會像預期的那樣工做,無論你聚焦在哪一個輸入框,始終顯示你的age信息。
  緣由在於,分配給onfocus事件的函數是閉包,他們由函數定義構成,從setupHelp函數的函數做用域獲取。三個閉包由循環所建立,每一個閉包具備同一個詞法環境,環境中包含一個變量item.help,當onfocus的回調執行時,item.help的值也隨之肯定,循環已經執行完畢,item對象已經指向了helpText列表的最後一項。解決這個問題的方法是使用更多的閉包,具體點就是提早使用一個封裝好的函數:

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

  上面代碼運行正常,回調此時不共享一個詞法環境,makeHelpCallback函數給每一個回調創造了一個詞法環境,詞法環境中的help指helpText數組中對應的字符串,使用匿名閉包來重寫的例子以下:

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // Immediate event listener attachment with the current value of item (preserved until iteration).
  }
}

setupHelp();

若是你不想使用閉包,你可使用ES6的let關鍵字

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

  這個例子使用let代替var,因此,每一個閉包綁定了塊級做用域,也就意味着不須要額外的閉包

性能考慮

  若是閉包在實際案例中是不被容許的,在一個函數中就不必定再建立一個函數,由於這會影響腳本的性能,例如處理的速度和內存的消耗。例如,當建立一個對象,對象的方法應該跟對象的原型聯繫起來而不是在對象的構造器裏定義,這是由於不管何時構造器被調用,方法都會被從新分配

下面一個例子

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

前面的代碼沒有充分利用閉包,咱們重寫以下

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

  然而,咱們不建議從新定義原型,下面的例子中,給原型分別定義方法而不是從新定義整個原型,這樣會改變constructor的指向。

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

  在前面兩個例子中,繼承原型能夠被全部對象所共享而且在每一個對象建立的同時都沒必要定義方法。

參考

相關文章
相關標籤/搜索