閉包與立即執行函數

在學習js的過程中,我們都會遇到閉包和立即執行函數的相關概念,今天就這兩個概念做一個大致的整理。

本文結構:

  • 閉包
    1. 閉包的概念
  • 立即執行函數
    1. 什么是立即執行函數
    2. 一個經典面試題
    3. es6 中的 let

一、閉包

我們先來看閉包的作用:閉包通常用來創建內部變量,使得這些變量不能被外部隨意修改,同時又可以通過指定的函數接口來操作。

對于不理解閉包的朋友來說,上面這句話并沒有什么用。所以我們通過一個簡單的例子來理解這個概念。

比如說我們現在要寫一個游戲,每個人初始都有5點精力,贏了就可以加1點精力,輸了就要減1點精力。

var amount=5;  //初始有5點精力
function add(){  //增加精力
  amount+=1;
  return amount;
}
function minus(){  //減少精力
  amount-=1;
  return amount;
}

//第一局贏了,調用增加精力函數
add();console.log(amount);  //結果為6

//第二局輸了,調用減少精力函數
minus();console.log(amount);  //結果為5

這樣就初步實現了我們想要的效果。

但是,這種做法存在這樣一個問題: 由于amount暴露在全局作用域下,是一個全局變量,所以任何人都可以隨意修改我的精力值。如果我們不小心在另一個js文件里也聲明了amount這樣一個全局變量并給它賦值,那么我們這里的amount就可能被覆蓋。

我們當前不希望amount被覆蓋,我們希望amount能“私有”一些,因此,我們對amount提出了兩點要求:

  1. 它不能被外部隨意修改;
  2. 需要能通過指定的方式去操作它。

首先,我們把上述代碼包在一個函數里:

function xxx(){
  var amount=5;
  function add(){
    amount+=1;
    return amount;
  }
  function minus(){
    amount-=1;
    return amount;
  }
}

這樣,amount的作用域就變為了函數xxx的內部,在函數外面無法訪問,這樣就實現了“不能被外部隨意修改”第一個要求。

但是,在函數xxx的外部,我們完全操作不到amount。因此,我們還需要給函數xxx留一個接口,讓我們能夠在函數外部去操作它。這該怎么辦呢?

經過一番苦思冥想,我們終于想到,是否可以利用函數的返回值呢?

function xxx(){
  var amount=5;
  function add(){
    amount+=1;
    console.log(amount);  //實時打印amount的當前值
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);  //實時打印amount的當前值
    return amount;
  }
  return add;  //為該函數添加一個返回值,返回add函數
}

然后調用函數xxx:

var outer_add=xxx();
outer_add();   //結果為6
outer_add();   //結果為7

我們發現,amount的值改變了,我們實現了我們想要實現的效果。

當調用函數xxx時,我們將xxx內部的add函數作為操作amount的一個接口返回,outer_add函數和xxx內部的add函數實際上是“一模一樣”的(因為它們都指向同一塊內存),而函數add可以訪問amount,因此我們可以通過outer_add來訪問amount。

需要注意的是,由于內部函數add在被訪問后一直處于被引用狀態,不能夠被垃圾回收。一旦我們不再使用outer_add函數,可以將其設置為null ,使其內存得到釋放。

實際上,這就是閉包的典型用法。現在我們再回頭看看開頭提到的閉包的作用,是否理解一些了呢?

細心的朋友可能已經發現了,按這種做法,我們只能返回一個函數add,那函數minus怎么辦呢?

我們這樣修改:

function xxx(){
  var amount=5;
  function add(){
    amount+=1;
    console.log(amount);
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);
    return amount;
  }
  return {
    outer_add:add,
    outer_minus:minus
  }
}

然后調用函數xxx:

var outer=xxx();
outer.outer_add();   //結果為6
outer.outer_add();   //結果為7
outer.outer_minus();   //結果為6

這樣,我們雖然不能直接操作amount這個局部變量,但可以通過函數xxx暴露出的函數outer_add和outer_minus來間接操作amount。

我們也可以這樣修改:

function xxx(){
  var amount=5;
  function add(){
    amount+=1;
    console.log(amount);
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);
    return amount;
  }
  window.outer_add=add;  //暴露outer_add函數,可間接訪問amount
  window.outer_minus=minus;  //暴露outer_minus函數,可間接訪問amount
}

然后調用函數xxx:

xxx();
outer_add();   //結果為6
outer_add();   //結果為7
outer_minus();   //結果為6

結果相同。


二、立即執行函數

1、什么是立即執行函數

立即執行函數,顧名思義,聲明一個函數,并立即執行它。在上面的代碼中,我們定義了一個函數xxx,并調用了它。這個函數用于頁面初始化,并且只會調用一次,因此我們可以使用立即執行函數來代替函數xxx。

改寫代碼如下:

function(){  //聲明了一個匿名函數
  var amount=5;
  function add(){
    amount+=1;
    console.log(amount);
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);
    return amount;
  }
  window.outer_add=add;  //暴露outer_add函數,可間接訪問amount
  window.outer_minus=minus;  //暴露outer_minus函數,可間接訪問amount
}();  //立即調用這個匿名函數

但是我們發現,瀏覽器竟然報錯了。

首先我們區分一下函數聲明和函數表達式:區分函數聲明和函數表達式最簡單的方法是看function關鍵字出現在聲明中的位置。如果function是聲明中的第一個詞,那么就是一個函數聲明,否則就是一個函數表達式。

所以,上述代碼之所以報錯,是因為瀏覽器認為函數聲明后再加 “(” 是錯誤的。所以我們需要通過加()、+、-、~、!等運算符,使上面的代碼變為函數表達式,這樣就不會報錯了,如:

(function(){
  var amount=5;
  function add(){
    amount+=1;
    console.log(amount);
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);
    return amount;
  }
  window.outer_add=add;  //暴露outer_add函數,可間接訪問amount
  window.outer_minus=minus;  //暴露outer_minus函數,可間接訪問amount
}());

然后我們就可以操作outer_add和outer_minus了:

outer_add();   //結果為6
outer_add();   //結果為7
outer_minus();   //結果為6

這就是立即執行函數的作用,創建一個局部作用域,使不想暴露在全局的變量變為局部變量。

2、一個經典面試題

題目如下:

for(var i=0;i<5;i++){
  setTimeout(function(){
    console.log(i);
  },1000);
}

結果:1s后,連續打印出5個5。

但是我們的本意是1s后分別打印出0、1、2、3、4,這時,我們就可以利用閉包和立即執行函數:

for(var i=0;i<5;i++){
  !function(j){
    setTimeout(function(){
      console.log(j);
    },1000);
  }(i);
}

結果:1s后,連續打印出0、1、2、3、4。

3、es6中的let

因為let聲明的變量是塊級作用域變量(而var聲明的變量為函數作用域變量),因此,我們可以利用let的特性來創建一個局部作用域,從而取代立即執行函數:

{  //用塊級作用域{}代替立即執行函數
  let amount=5;
  function add(){
    amount+=1;
    console.log(amount);
    return amount;
  }
  function minus(){
    amount-=1;
    console.log(amount);
    return amount;
  }
  window.outer_add=add;  //暴露outer_add函數,可間接訪問amount
  window.outer_minus=minus;  //暴露outer_minus函數,可間接訪問amount
}

然后我們就可以操作outer_add和outer_minus了:

outer_add();   //結果為6
outer_add();   //結果為7
outer_minus();   //結果為6

塊級作用域的出現,實際上使得獲得廣泛應用的立即執行函數表達式(IIFE)不再必要了。

這篇博客就先整理到這里。由于個人水平有限,博客錯誤之處,煩請指正!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。