![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cGcq5yN5YTO3ATM3cTY1EmYhNzMzYzXxIDNwATM4IzLclDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.jpg)
一、引子
閉包(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)
}
本文完〜