淺談JavaScript中的閉包

廢話

閉包,這個詞甚至讓很多人看到都覺得頭疼,有的人說這東西非常好用,有人說不懂是啥東西...

引用一段JavaScript權(quán)威指南上一段話:

和其他大多數(shù)現(xiàn)代編程語言一樣,JavaScript也采用詞法作用域(lexical scoping).也就是,函數(shù)的執(zhí)行依賴于變量作用域,這個作用域是函數(shù)定義時決定的.而不是函數(shù)執(zhí)行時決定的.為了實現(xiàn)這種詞法作用域,JavaScript函數(shù)對象的內(nèi)部狀態(tài)不僅包含函數(shù)的代碼邏輯,還必須引用當前的作用域鏈(scope chain).函數(shù)可以通過作用域鏈(scope chain)相互關(guān)聯(lián)起來,函數(shù)內(nèi)部的變量都可以保存在函數(shù)作用域內(nèi),這種特性稱為"閉包”.從某種角度上來說,JavaScript中所有的函數(shù)都是閉包:他們都是對象,他們都關(guān)聯(lián)到作用域鏈.

這段話,很多人即使讀了幾遍以后還是云里霧里,其實關(guān)于這段話,你只需要記住一句:

函數(shù)的執(zhí)行依賴于變量作用域,這個作用域是函數(shù)定義時決定的,而不是函數(shù)執(zhí)行時決定的.

那么接下我們通過代碼解析,來一步一步解釋何為閉包.

閉包

可能會有一些老司機會告訴你,在JavaScript中閉包就是一個嵌套函數(shù)...

用一段簡單粗暴的代碼來告訴你,啥是閉包:

function showClosure() {
  var str = "閉包";
  return function() { // 這就是一個閉包
    return str;
  };
}

showClosure();

我們來分析一下這段代碼:上面這段代碼定義了一個showClosure()函數(shù),該函數(shù)又將一個匿名函數(shù)作為返回值返回.這里就形成了一個閉包.在最后調(diào)用showClosure()函數(shù)的時候其定義時的作用域鏈仍然是有用的,這并不會影響里面的閉包.而且showClosure()返回了一個匿名函數(shù)/閉包,這里有很多微妙的事情可以說:

  • 閉包的值捕獲
  • 此時垃圾機制的運作和作用域鏈的關(guān)系
  • 值捕獲導(dǎo)致的內(nèi)存問題
  • this關(guān)鍵字在其中的使用

接下來我們一一分析;

值捕獲

值捕獲是閉包的一個非常強大的特性,閉包可以捕獲到局部變量(或參數(shù)),并一直保存下來,駐留在內(nèi)存中;

這個特性可以解決全局變量的污染問題(全局變量污染導(dǎo)致應(yīng)用程序不可預(yù)測性,每個模塊都可調(diào)用必將引來災(zāi)難, 所以推薦使用私有的,封裝的局部變量),在很多開源框架中普遍使用;

我們再看看上面那段代碼,然后根據(jù)代碼來分析:

function showClosure() {
  var str = "閉包";
  return function() {
    return str;
  };
}

console.log(showClosure()());

注意,在外圍函數(shù)中有一個值為”閉包”的局部變量str.按照我們常規(guī)的思維,在showClosure函數(shù)執(zhí)行到最后一個”}”的時候,里面所有的值應(yīng)該都會被垃圾回收機制回收掉才對,然而我們調(diào)用該函數(shù)的時候其結(jié)果仍然能夠返回str的值.這就是因為里面的閉包捕獲了str,導(dǎo)致其會駐留在內(nèi)存中不被釋放,所以我們后面調(diào)用的時候仍然有值;很多人很是不解,這其實跟作用域鏈原理有關(guān):

JavaScript中的作用域鏈(scope chain)是一個列表對象,而不是像其他語言是綁定的棧(stack);每次調(diào)用JavaScript函數(shù)的時候,都會為之創(chuàng)建一個新的對象來保存局部變量,把這個對象添加至作用域鏈中;當函數(shù)返回的時候,就從作用域鏈中將這個綁定的變量的對象刪除;如果不存在嵌套的函數(shù),也就沒有其他引用指向這個綁定對象,它就會被當做垃圾回收掉;如果定義了嵌套的函數(shù),每一個嵌套函數(shù)又都各自對應(yīng)一個作用域鏈,并且這個作用域鏈指向一個變量綁定對象.但如果這些嵌套的函數(shù)對象在外部函數(shù)中保存下來,那么它們呢也會和所指向的變量綁定對象一樣當做垃圾回收掉;但是如果這個函數(shù)定義了嵌套函數(shù),并且將它作為返回值或者被存儲下來,這是就會有一個外部引用指向這個嵌套的函數(shù).它就不會被當做垃圾回收,并且他所指向的變量綁定對象也不會被當做垃圾回收掉.

所以,其實是在調(diào)用showClosure函數(shù)的時候,str并沒有被釋放,因為他被作為返回值函數(shù)的返回值返回了(有點兒繞??);

看到這里有些同學(xué)可能就會看出另一個問題,閉包會引起內(nèi)存泄露,因為他會捕獲一些值存儲下來,導(dǎo)致后面不能釋放,除非瀏覽器被關(guān)閉;

內(nèi)存問題

既然值被捕獲了,系統(tǒng)不能自動釋放,那么我們就自己手動釋放嘛;只需要將需要釋放的對象手動置為null即可;

示例:

function showClosure() {
  var aDiv = document.getElementById("aDiv");
  aDiv.onclick = function() {
    console.log(aDiv.innerHTML);
  }
  
  aDiv = null; // 解除引用,如果沒有解除,在瀏覽器被關(guān)閉的時候才能被回收
}

閉包中的this

在使用閉包的時候有一個非常需要注意的東西:魔性的this!

在JavaScript中這個this非常容易用錯,即使是多年的老司機也是經(jīng)常翻車.

this如果在全局中調(diào)用其值為全局對象(非嚴格模式)或undefined(嚴格模式).那么大多數(shù)人會認為在閉包中使用this返回的應(yīng)該是當前函數(shù)對象或者是最外層的函數(shù)對象;然而在閉包中卻不是這樣,在閉包中this會隨著調(diào)用對象的不同所指代的對象不同,而非外層的函數(shù)對象;所以這里使用this的時候要使用一個小技巧,那就是把最外層對象的this保存下來;

看代碼:

// 注意這段代碼是執(zhí)行在非嚴格模式下的
// 在嚴格模式下這種方式會報錯,也就是說嚴格模式下不允許你這么寫
var name = "NAME";
var person = {
    name : "name",
    getUserName : function() {
        return function() {
            return this.name;
        };
    }
};

console.log(person.getUserName()());

這個東西在很多面試題上經(jīng)??吹?其實就是考你對閉包和作用域的理解;

執(zhí)行結(jié)果如下:

NAME

看到?jīng)],這種情況下this會指向全局對象,這樣this.name的值就是”NAME”;這如果想要讓this.name的值為”name”有兩種方法:

第一種,在調(diào)用的時候使用call函數(shù)強制傳一個對象給他.

console.log(person.getUserName().call(person));

第二種,保存外層對象的this:

var name = "NAME";
var person = {
    name : "name",
    getUserName : function() {
        var self = this; // 將this保存至一個變量中
        return function() {
            return self.name;
        };
    }
};

console.log(person.getUserName()());

所以,還是記住那句話:函數(shù)的執(zhí)行依賴于變量作用域,這個作用域是函數(shù)定義時決定的,而不是函數(shù)執(zhí)行時決定的.

生命不息,折騰不止...
I'm not a real coder,but i love it so much!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容