1. 程式人生 > >對JavaScript中閉包的理解

對JavaScript中閉包的理解

func web瀏覽器 並且 清晰 分享 數量 接下來 我們 回收

相信很多人都有看過關於閉包的文章,但是真正意義上的了解清楚的也不多,今天我們就來談談對閉包的理解。

閉包在JavaScript中一直是一個很重要的存在,閉包很重要但是又很難理解,起初我也是這樣認為,但只要真的清楚之後,你會覺得很有趣。

我們先來看一個閉包的例子:

1 function foo() {
2  let a = 2;
3  function bar() {
4   console.log(a);
5  }
6  return bar;
7 }
8 let baz = foo();
9 baz();

大家肯定都寫過類似的代碼,相信很多小夥伴也知道這段代碼應用了閉包,但是,為什麽會產生閉包,閉包又是在哪裏?

回答上面的問題,首先必須先知道閉包是什麽,才能分析出閉包為什麽產生和閉包到底在哪?

當一個函數能夠記住並訪問到其所在的詞法作用域及作用域鏈,特別強調是在其定義的作用域外進行的訪問,此時該函數和其上層執行上下文共同構成閉包。

需要明確幾點:

  1、閉包一定是函數對象

  2、閉包和詞法作用域,作用域鏈,垃圾回收機制息息相關

  3、當函數一定是在其定義的作用域外進行的訪問時,才產生閉包

  4、閉包是由該函數和其上層執行上下文共同構成

閉包是什麽,我們說清楚了,下面我們看下閉包是如何產生的。

現在我假設JS引擎執行到這行代碼:

let baz = foo();

  

此時,JS的作用域是這樣的:

技術分享圖片

這個時候foo函數已經執行完,JS的垃圾回收機制應該會自動將其標記為"離開環境",等待回收機制下次執行,將其內存進行釋放(標記清除)。

但是,我們仔細看圖中粉色的箭頭,我們將bar的引用指向baz,正是這種引用賦值,阻止了垃圾回收機制將foo進行回收,從而導致bar的整條作用域鏈都被保存下來。

接下來,baz()執行,bar進入執行棧,閉包(foo)形成,此時bar中依舊可以訪問到其父作用域氣泡中的變量a。

這樣說可能不是很清晰,接下來我們借助chrome的調試工具看下閉包產生的過程。

當JS引擎執行到這行代碼let baz = foo();時:

技術分享圖片

圖中所示,let baz = foo();已經執行完,即將執行baz();,此時Call Stack中只有全局上下文。

接下來baz();執行:

技術分享圖片

我們可以看到,此時bar進入Call Stack中,並且Closure(foo)形成。

針對上面我提到的幾點進行下說明:

  1、上述第二點(閉包和詞法作用域,作用域鏈,垃圾回收機制息息相關)大家應該都清楚了

  2、上述第三點,當函數baz執行時,閉包才生成

  3、上述第四點,閉包是foo,並不是bar,很多書(《you dont know JavaScript》《JavaScript高級程序設計》)中,都強調保存下來的引用,即上例中的bar是閉包,而chrome認為被保存下來的封閉空間foo是閉包

仔細想來,在我們作用域模型中,作用域鏈讓我們的內部bar氣泡能夠"看到"外面的世界,而閉包則讓我們的外部作用域能夠"關註到"內部的情況成為可能。可見,只要我們願意,內心世界和外面世界是可以相通的。

技術分享圖片

使用閉包時的註意事項:

閉包,在JS中絕對是一個高貴的存在,它讓很多不可能實現的代碼成為可能,但是物雖好,也要合理使用,不然不但不能達到我們想要的效果,有的時候可能還會適得其反。

內存泄漏(Memory Leak)

JavaScript分配給Web瀏覽器的可用內存數量通常比分配給桌面應用程序的少,這樣做主要是防止JavaScript的網頁耗盡全部系統內存而導致系統崩潰。

因此,要想使頁面具有更好的性能,就必須確保頁面占用最少的內存資源,也就是說,我們應該保證執行代碼只保存有用的數據,一旦數據不再有用,我們就應該讓垃圾回收機制對其進行回收,釋放內存。

我們現在都知道了閉包阻止了垃圾回收機制對變量進行回收,因此變量會永遠存在內存中,即使當變量不再被使用時,這樣會造成內存泄漏,會嚴重影響頁面的性能。因此當變量對象不再適用時,我們要將其釋放。

我們拿上面代碼舉例:

function foo() {
 let a = 2;
 function bar() {
   console.log(a);
 }
 return bar;
}
let baz = foo();
baz();//baz指向的對象會永遠存在堆內存中
baz = null;//如果baz不在使用,將其指向的對象釋放

對JavaScript中閉包的理解