在學習js的過程中,我們都會遇到閉包和立即執行函數的相關概念,今天就這兩個概念做一個大致的整理。
本文結構:
-
閉包
- 閉包的概念
-
立即執行函數
- 什么是立即執行函數
- 一個經典面試題
- 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提出了兩點要求:
- 它不能被外部隨意修改;
- 需要能通過指定的方式去操作它。
首先,我們把上述代碼包在一個函數里:
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)不再必要了。
這篇博客就先整理到這里。由于個人水平有限,博客錯誤之處,煩請指正!