JavaScript 的 閉包

詞法作用域:

首先我們看一個最簡單的例子:

var x = 100;
function fn(){
  console.log(x);
}
fn();    // 100

毫無疑問,"fn()" 函數是可以訪問到外部定義的變量 "x"。函數被創建時,都會創建其"作用域"。"fn" 函數被創建,其作用域內未聲明變量 "x" ,只能到上一級的作用域(這裏是全局作用域)找,這裏的 "x" 稱之爲 "自由變量"

 

簡單的閉包:

function fn() {
  var x = 10;
  return function (n) {
    return x > n ? x : n
  }
}
let moreThanTen = fn()
console.log( moreThanTen(9) )
console.log( moreThanTen(11) )

上述例子中,"fn" 內部創建了變量 "x" 和返回一個匿名函數,匿名函數使用到父級的自由變量 "x" 。

正常來說,第七行執行 "fn" 函數之後,其函數作用域應該銷燬,但匿名函數需要用到父級變量,因而產生了閉包。

閉包是由函數以及創建該函數的詞法環境組合而成。這個環境包含了這個閉包創建時所能訪問的所有局部變量

 

用閉包模擬私有方法:

var Counter = (function() {
  // 私有變量
  var privateCounter = 0;
  // 私有方法,用於修改私有變量
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function(n) {
      changeBy(n);
    },
    decrement: function(n) {
      changeBy(-n);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();
console.log(Counter.value());    // 0
Counter.increment(3);
console.log(Counter.value());    // 3
Counter.decrement(1);
console.log(Counter.value());    // 2

這個環境中包含兩個私有項:名爲 privateCounter 的變量和名爲 changeBy 的函數。這兩項都無法在這個匿名函數外部直接訪問。必須通過匿名函數返回的三個公共函數訪問。這三個公共函數是共享同一個環境的閉包。

以這種方式使用閉包,提供了許多與面向對象編程相關的好處 —— 特別是數據隱藏和封裝。

 

在循環中創建閉包:一個常見錯誤

首先看一個官方錯誤例子

<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();

運行這段代碼後,您會發現它沒有達到想要的效果。無論焦點在哪個input上,顯示的都是關於年齡的信息。

原因是賦值給 onfocus 的是閉包。這些閉包是由他們的函數定義和在 setupHelp 作用域中捕獲的環境所組成的。這三個閉包在循環中被創建,但他們共享了同一個詞法作用域,在這個作用域中存在一個變量item。當onfocus的回調執行時,item.help的值被決定。由於循環在事件觸發之前早已執行完畢,變量對象item(被三個閉包所共享)已經指向了helpText的最後一項。可以理解爲,三個 onfocus 函數共享一個閉包環境

最爲簡單的解決辦法是把循環中的 "var" 關鍵字改爲 "let"。如下:

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

或者使用匿名閉包:

for (var i = 0; i < helpText.length; i++) {
  (function() {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  })(); // 馬上把當前循環項的item與事件回調相關聯起來
}

 

性能考量:

如果不是某些特定任務需要使用閉包,在其它函數中創建函數是不明智的,因爲閉包在處理速度和內存消耗方面對腳本性能具有負面影響。

例如,在創建新的對象或者類時,方法通常應該關聯於對象的原型,而不是定義到對象的構造器中。原因是這將導致每次構造器被調用時,方法都會被重新賦值一次(也就是,每個對象的創建)。

 

關於循環體內的 "var" 與 "let" :

function fnVar() {
  for (var i = 0; i < 5; i++) { }
  console.log(i)
}
function fnLet() {
  for (let i = 0; i < 5; i++) { }
  console.log(i)
}
fnVar()       // 5
// fnLet()    // 會報錯 

在ES6 之前,JavaScript 是沒有塊級作用域,上述例子證明了 "var" 聲明的變量在塊作用域仍然存在。

ES6 中新增的 "let" 關鍵字提供了塊級作用域,其聲明的變量在塊外不能訪問。

 

附:MDN的閉包講解

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