javascript中的閉包closure詳解

簡介

閉包closure是javascript中一個很是強大的功能。所謂閉包就是函數中的函數,內部函數能夠訪問外部函數的做用域範圍,從而可使用閉包來作一些比較強大的工做。javascript

今天將會給你們詳細介紹一下閉包。java

函數中的函數

咱們提到了函數中的函數能夠訪問父函數做用域範圍的變量,咱們看一個例子:閉包

function parentFunction() {
  var address = 'flydean.com'; 
  function alertAddress() { 
    alert(address); 
  }
  alertAddress();
}
parentFunction();

上面的例子中,咱們在parentFunction中定義了一個變量address,在parentFunction內部定義了一個alertAddress方法,在該方法內部訪問外部函數中定義的address變量。函數

上面代碼運行是沒問題的,能夠正確的訪問到數據。性能

Closure閉包

函數中的函數有了,那麼什麼是閉包呢?字體

咱們看下面的例子:this

function parentFunction() {
  var address = 'flydean.com'; 
  function alertAddress() { 
    alert(address); 
  }
  return alertAddress;
}
var myFunc = parentFunction();
myFunc();

這個例子和第一個例子很相似,不一樣之處就是咱們將內部函數返回了,而且賦值給了myFunc。prototype

接下來咱們直接調用了myFunc。code

myFunc中訪問了parentFunction中的address變量,雖然parentFunction已經執行完畢返回。對象

可是咱們在調用myFunc的時候,任然能夠訪問到address變量。這就是閉包。

閉包的這個特性很是擁有,咱們可使用閉包來生成function factory,以下所示:

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

其中add5和add10都是閉包,他們是由makeAdder這個function factory建立出來的。經過傳遞不一樣的x參數,咱們獲得了不一樣的基數的add方法。

最終生成了兩個不一樣的add方法。

使用function factory的概念,咱們能夠考慮一個閉包的實際應用,好比咱們在頁面上有三個button,經過點擊這些button可實現修改字體的功能。

咱們能夠先經過function factory來生成三個方法:

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

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

有了這三個方法,咱們把DOM元素和callback方法綁定起來:

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

使用閉包實現private方法

對比java來講,java中有private訪問描述符,經過private,咱們能夠指定方法只在class內部訪問。

固然,在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());  // 0.

counter.increment();
counter.increment();
console.log(counter.value());  // 2.

counter.decrement();
console.log(counter.value());  // 1.

咱們在父function中定義了privateCounter屬性和changeBy方法,可是這些方法只可以在內部function中訪問。

咱們經過閉包的概念,將這些屬性和方法封裝起來,暴露給外部使用,最終達到了私有變量和方法封裝的效果。

閉包的Scope Chain

對於每一個閉包來講,都有一個做用域範圍,包括函數自己的做用域,父函數的做用域和全局的做用域。

若是咱們在函數內部嵌入了新的函數,那麼就會造成一個做用域鏈,咱們叫作scope chain。

看下面的一個例子:

// global scope
var e = 10;
function sum(a){
  return function(b){
    return function(c){
      // outer functions scope
      return function(d){
        // local scope
        return a + b + c + d + e;
      }
    }
  }
}

console.log(sum(1)(2)(3)(4)); // log 20

閉包常見的問題

第一個常見的問題就是在循環遍歷中使用閉包,咱們看一個例子:

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

上面的例子中,咱們建立了一個setupHelp函數,setupHelp中,onfocus方法被賦予了一個閉包,因此閉包中的item能夠訪問到外部function中定義的item變量。

由於在循環裏面賦值,因此咱們實際上建立了3個閉包,可是這3個閉包共享的是同一個外部函數的做用域範圍。

咱們的本意是,不一樣的id觸發不一樣的help消息。可是若是咱們真正執行就會發現,不論是哪個id,最終的消息都是最後一個。

由於onfocus是在閉包建立完畢以後纔會觸發,這個時候item的值其實是變化的,在循環結束以後,item的值已經指向了最後一個元素,因此所有顯示的是最後一條數據的help消息。

怎麼解決這個問題呢?

最簡單的辦法使用ES6中引入的let描述符,從而將item定義爲block的做用域範圍,每次循環都會建立一個新的item,從而保持閉包中的item的值不變。

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

還有一種方法,就是再建立一個閉包:

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

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

這裏用到了以前咱們提到的function factory的概念,咱們爲不一樣的閉包建立了不一樣的做用域環境。

還有一種方法就是將item包含在一個新的function做用域範圍以內,從而每次建立都是新的item,這個和let的原理是類似的:

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

第二個常見的問題就是內存泄露。

function parentFunction(paramA)
 {
 var a = paramA;
 function childFunction()
 {
 return a + 2;
 }
 return childFunction();
 }

上面的例子中,childFunction引用了parentFunction的變量a。只要childFunction還在被使用,a就沒法被釋放,從而致使parentFunction沒法被垃圾回收。

閉包性能的問題

咱們定義了一個對象,而且經過閉包來訪問其私有屬性:

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

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

上面的對象會有什麼問題呢?

上面對象的問題就在於,對於每個new出來的對象,getName和getMessage方法都會被複制一份,一方面是內容的冗餘,另外一方面是性能的影響。

一般來講,咱們將對象的方法定義在prototype上面:

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;
};
注意,咱們不要直接重寫整個prototype,這樣會致使未知的錯誤,咱們只須要根據須要重寫特定的方法便可。

總結

閉包是JS中很是強大和有用的概念,但願你們可以喜歡。

本文做者:flydean程序那些事

本文連接:http://www.flydean.com/js-closure/

本文來源:flydean的博客

歡迎關注個人公衆號:「程序那些事」最通俗的解讀,最深入的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!

相關文章
相關標籤/搜索