JS的閉合(Closure)
定義
MDN:函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起構成閉包(closure)。
JS高程: 閉包是指有權訪問另一個函數作用域中的變數的函數。
Javascript權威指南:函數對象可以通過作用域關聯起來,函數體內的變數都可以保存在函數作用域內,這在電腦科學文獻中稱為「閉包」,所有的javascirpt函數都是閉包
你不知道的JS(上):當函數可以記住並訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前作用域外執行
瀏覽器原理與實踐:當通過調用一個外部函數返回一個內部函數後,即使該外部函數已經執行結束了,但是內部函數引用外部函數的變數依然保存在記憶體中,我們就把這些變數的集合稱為閉包。比如外部函數是 foo,那麼這些變數的集合就稱為 foo 函數的閉包。
各種書籍上對閉包的定義各不相同,但是我們沒必要太過於糾結概念,只要我們深入理解他的含義和使用就可以了
產生原因
閉包產生與如下兩個方面有關
- 根據詞法作用域的規則,內部作用域可以訪問外部作用域
- GC(垃圾回收)機制,如果變數被引用那麼GC在回收時並不會回收該變數
實例
function foo() { let test1 = '變數1' const test2 = '變數2' let test3 = '變數3' var innerBar = { getName: function () { console.log(test1) return test2 }, } return innerBar } var bar = foo() bar.getName()
我們可以通過控制台的Source中的scope看到閉包
右邊 Scope 項就體現出了作用域鏈的情況:Local 就是當前的 getName 函數的作用域,Closure(foo) 是指 foo 函數的閉包,最下面的 Global 就是指全局作用域,從「Local–>Closure(foo)–>Global」就是一個完整的作用域鏈。
注意⚠️:只有我們在內部函數中使用的變數才會被加入閉包(Closure)中。
我們來看下範例中產生閉包時執行棧的情況:
- 當執行到 foo 函數內部的return innerBar這行程式碼時調用棧的情況
- 當 foo 函數執行完成之後,其整個調用棧的狀態(注意⚠️:此時foo執行上下文已出棧)
- 當執行到bar.getName()時的棧狀態
通過上面三個圖我們可以清晰的看到當前作用域鏈 是Local–>Closure(foo)–>Global。
用途
實現私有變數
function Animal( ){ //私有變數 var series = "哺乳動物"; function run( ){ console.log("Run!!!"); } //特權方法 this.getSeries = function(){ return series; }; }
實現模組模式
var Counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); console.log(Counter.value()); /* logs 0 */ Counter.increment(); Counter.increment(); console.log(Counter.value()); /* logs 2 */ Counter.decrement(); console.log(Counter.value()); /* logs 1 */
缺陷
如果引用閉包的函數是一個全局變數,那麼閉包會一直存在直到頁面關閉;但如果這個閉包以後不再使用的話,就會造成記憶體泄漏。(如果引用閉包的函數是個局部變數,等函數銷毀後,在下次 JavaScript 引擎執行垃圾回收時,判斷閉包這塊內容如果已經不再被使用了,那麼 JavaScript 引擎的垃圾回收器就會回收這塊記憶體)
規避:如果該閉包會一直使用,那麼它可以作為全局變數而存在;但如果使用頻率不高,而且佔用記憶體又比較大的話,那就盡量讓它成為一個局部變數。