對JavaScript中閉包的理解
相信很多人都有看過關於閉包的文章,但是真正意義上的了解清楚的也不多,今天我們就來談談對閉包的理解。
閉包在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中閉包的理解