天天看點

深入淺出Javascript閉包

深入淺出Javascript閉包

一、引子

閉包(closure)是 JavaScript 語言的一個難點,面試時常被問及,也是它的特色,很多進階應用都要依靠閉包實作。本文盡可能用簡單易懂的話,講清楚閉包的概念、形成條件及其常見的面試題。

我們先來看一個例子:

var n = 999;
function f1() {
console.log(n);
}
f1() // 999      

上面代碼中,函數f1可以讀取全局變量n。但是,函數外部無法讀取函數内部聲明的變量。

function f1() {
var n = 999;
}
console.log(n)
// Uncaught ReferenceError: n is not defined      

上面代碼中,函數f1内部聲明的變量n,函數外是無法讀取的。

如果有時需要得到函數内的局部變量。正常情況下,這是辦不到的,隻有通過變通方法才能實作。那就是在函數的内部,再定義一個函數。

function f1() {
var n = 999;
function f2() {
  console.log(n); // 999
 }
}      

上面代碼中,函數f2就在函數f1内部,這時f1内部的所有局部變量,對f2都是可見的。既然f2可以讀取f1的局部變量,那麼隻要把f2作為傳回值,我們不就可以在f1外部讀取它的内部變量了嗎!

二、閉包是什麼

我們可以對上面代碼進行如下修改:

function f1(){
   var a = 999;
   function f2(){
    console.log(a);
   }
   return f2; // f1傳回了f2的引用
   }
   var result = f1(); // result就是f2函數了
   result();  // 執行result,全局作用域下沒有a的定義,
         //但是函數閉包,能夠把定義函數的時候的作用域一起記住,輸出999      

上面代碼中,函數f1的傳回值就是函數f2,由于f2可以讀取f1的内部變量,是以就可以在外部獲得f1的内部變量了。

閉包就是函數f2,即能夠讀取其他函數内部變量的函數。由于在JavaScript語言中,隻有函數内部的子函數才能讀取内部變量,是以可以把閉包簡單了解成“定義在一個函數内部的函數”。

閉包最大的特點,就是它可以“記住”誕生的環境,比如f2記住了它誕生的環境f1,是以從f2可以得到f1的内部變量。在本質上,閉包就是将函數内部和函數外部連接配接起來的一座橋梁。

那到底什麼是閉包呢?

當函數可以記住并通路所在的詞法作用域,即使函數是在目前詞法作用域之外執行,這就産生了閉包。 ----《你不知道的Javascript上卷》

我個人了解,閉包就是函數中的函數(其他語言不能函數再套函數),裡面的函數可以通路外面函數的變量,外面的變量的是這個内部函數的一部分。

閉包形成的條件

  • 函數嵌套
  • 内部函數引用外部函數的局部變量

三、閉包的特性

每個函數都是閉包,每個函數天生都能夠記憶自己定義時所處的作用域環境。把一個函數從它定義的那個作用域,挪走,運作。這個函數居然能夠記憶住定義時的那個作用域。不管函數走到哪裡,定義時的作用域就帶到了哪裡。接下來我們用兩個例子來說明這個問題:

//例題1
var inner;
function outer(){
var a=250;
inner=function(){
alert(a);//這個函數雖然在外面執行,但能夠記憶住定義時的那個作用域,a是250
  }
}
outer();
var a=300;
inner();//一個函數在執行的時候,找閉包裡面的變量,不會理會目前作用域。      
//例題2
function outer(x){
  function inner(y){
  console.log(x+y);
  }
return inner;
}
var inn=outer(3);//數字3傳入outer函數後,inner函數中x便會記住這個值
inn(5);//當inner函數再傳入5的時候,隻會對y指派,是以最後彈出8      

四、閉包的記憶體洩漏

棧記憶體提供一個執行環境,即作用域,包括全局作用域和私有作用域,那他們什麼時候釋放記憶體的?

  • 全局作用域----隻有當頁面關閉的時候全局作用域才會銷毀
  • 私有的作用域----隻有函數執行才會産生

一般情況下,函數執行會形成一個新的私有的作用域,當私有作用域中的代碼執行完成後,我們目前作用域都會主動的進行釋放和銷毀。但當遇到函數執行傳回了一個引用資料類型的值,并且在函數的外面被一個其他的東西給接收了,這種情況下一般形成的私有作用域都不會銷毀。

如下面這種情況:

function fn(){
var num=100;
return function(){
  }
}
var f=fn();//fn執行形成的這個私有的作用域就不能再銷毀了      

也就是像上面這段代碼,fn函數内部的私有作用域會被一直占用的,發生了記憶體洩漏。所謂記憶體洩漏指任何對象在您不再擁有或需要它之後仍然存在。閉包不能濫用,否則會導緻記憶體洩露,影響網頁的性能。閉包使用完了後,要立即釋放資源,将引用變量指向null。

接下來我們看下有關于記憶體洩漏的一道經典面試題:

function outer(){
  var num=0;//内部變量
  return function add(){//通過return傳回add函數,就可以在outer函數外通路了
  num++;//内部函數有引用,作為add函數的一部分了
  console.log(num);
  };
 }
  var func1=outer();
  func1();//實際上是調用add函數, 輸出1
  func1();//輸出2 因為outer函數内部的私有作用域會一直被占用
  var func2=outer();
  func2();// 輸出1  每次重新引用函數的時候,閉包是全新的。
  func2();// 輸出2      

五、閉包的作用

1.可以讀取函數内部的變量。

2.可以使變量的值長期儲存在記憶體中,生命周期比較長。是以不能濫用閉包,否則會造成網頁的性能問題

3.可以用來實作js子產品。

js子產品:具有特定功能的js檔案,将所有的資料和功能都封裝在一個函數内部(私有的),隻向外暴露一個包信n個方法的對象或函數,子產品的使用者,隻需要通過子產品暴露的對象調用方法來實作對應的功能。

具體請看下面的例子:

//index.html檔案
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
  myModule2.doSomething()
  myModule2.doOtherthing()
</script>      
//myModule.js檔案
(function () {
  var msg = 'Beijing'//私有資料
  //操作資料的函數
  function doSomething() {
    console.log('doSomething() '+msg.toUpperCase())
  }
  function doOtherthing () {
    console.log('doOtherthing() '+msg.toLowerCase())
  }
  //向外暴露對象(給外部使用的兩個方法)
  window.myModule2 = {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
})()      

六、閉包的運用

我們要實作這樣的一個需求: 點選某個按鈕, 提示"點選的是第n個按鈕",此處我們先不用事件代理:

.....
<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
   var btns = document.getElementsByTagName('button')
    for (var i = 0; i < btns.length; i++) {
      btns[i].onclick = function () {
        console.log('第' + (i + 1) + '個')
      }
    }
</script>      

萬萬沒想到,點選任意一個按鈕,背景都是彈出“第四個”,這是因為i是全局變量,執行到點選事件時,此時i的值為3。那該如何修改,最簡單的是用let聲明i

for (let i = 0; i < btns.length; i++) {
      btns[i].onclick = function () {
        console.log('第' + (i + 1) + '個')
      }
    }      

另外我們可以通過閉包的方式來修改:

for (var i = 0; i < btns.length; i++) {
      (function (j) {
        btns[j].onclick = function () {
          console.log('第' + (j + 1) + '個')
        }
      })(i)
    }      

本文完〜

繼續閱讀