1. 程式人生 > 其它 >再來看看閉包

再來看看閉包

技術標籤:JSjavascript

再來看看閉包

什麼是閉包

首先,先放入紅寶書上的一個定義:閉包是指有權訪問另一個函式作用域中的變數的函式
換句話說,A 函式可以訪問 B 函式的變數,即 A 函式定義在函式 B 內部

function func1(){
    var str='closure'
    return function(){
        console.log(str)
    }
}
var demo=func1()
demo()// closure

為什麼會產生閉包

如上面的程式碼,當我們執行 demo()的時候,程式碼直譯器會在當前的作用域中查詢,如果沒有就會當父級作用域去查詢,直到找到該變數或者不存在。其實核心就是作用域鏈的問題,從函式自身的作用域一直往外查詢(最外面當然就是全域性作用域 window 了)。這裡還是做一個不完美的比喻,沿著作用域鏈查詢變數的過程,這就像畫一個洋蔥,先把最裡層洋蔥核的那部分畫上,在把外層一層層的畫上,直到把最外層的表皮畫上

一個 tricky 的地方

看著上面的程式碼,是不是一定要 return 才是閉包了?其實不是的,這只是表現形式而已,只要當前函式可以指向父級作用域,那麼就是閉包

var innerFunc;
function outFunc(){
    var str='closure'
    innerFunc=function(){
        console.log(str)
    }
}
outFunc()
innerFunc()

在這個 demo 中 outFunc()的執行相當於就是建立了一個塊作用域環境,外部定義的 innerFunc 相當於就是之前的 return 的功能——暴露給我們一個“介面”,可以使用它。當執行 innerFunc()的時候,由於有 outFunc()建立的塊級作用域的存在,故也能一層層的向著上一級父級作用域查詢變數。

一個長問的 demo,用閉包來解釋

demo1:
for(var i = 1; i <= 5; i ++){
  setTimeout(function() {
    console.log(i)
  }, 0)
}

demo2:
for(let i = 1; i <= 5; i ++){
  setTimeout(function() {
    console.log(i)
  }, 0)
}

為什麼 demo1 這裡輸出全部是 6
Answer:

  • setTimeout 為非同步任務,主執行緒會先把 for()迴圈 i++這部分同步任務執行完後才去執行非同步任務,因此迴圈結束後 setTimeout 中的回撥才依次執行。

  • 對於 setTimeout 函式而言,它也是一種閉包,往上找它的父級作用域鏈就是 window,變數 i 掛載在了 window 上,開始執行 setTimeout 之前變數 i 已經就是 6 了,因此最後輸出的值就都是 6了。


那為什麼對於 demo2,用了 let 結果就變了呢?
其實這裡最為主要的區別在於,因為 let 和 for 一起建立了一個塊作用域,對於 setTimeout 函式而言,其最近的父級作用域鏈就是 for 包裹的這層塊級作用域鏈,在這層之外才是 window,簡而言之,就是因為 let 的特性,使得 for 包裹的這層也生層了一個作用域

在深入一點,從 JS 引擎的角度談下閉包的使用

關於 JS V8 引擎的常識內容,可以看看我的上一篇文章,上一篇文章中對 JS 引擎進行了一個簡單介紹包括幾個易混淆的關係以及引擎的組層部分。在程式碼的編寫中,閉包使用的場景會特別多而且複雜,有一個問題於閉包而言便是閉包會使一些變數一直儲存在記憶體中不會自動釋放,所以如果大量使用的話,就會消耗大量記憶體,從而影響網頁效能。因此在這裡想再從 JS 引擎的角度去談談,閉包與引擎的關係,換句話說,從引擎的角度理解閉包後,我們在使用閉包的時候才能更考慮編碼效能。

先來看看紅寶書上,對於垃圾收集原理的一句話講解:“找出那些不再繼續使用的變
量,然後釋放其佔用的記憶體。為此,垃圾收集器會按照固定的時間間隔(或程式碼執行中預定的收集時間),
週期性地執行這一操作。”

還是對第一個 demo 進行分析,對於變數 str 的生命週期進行思考,如果內部沒有匿名函式(或者說閉包現象),當函式 func1()執行完後,儲存區域性變數 str 的記憶體就會被釋放,but 由於閉包的原因,這部分記憶體依然被 str 佔用,直到內部函式執行。假如說,如果執行完外部函式後很久才執行內部函式,那麼相比非閉包情況,很長一段時間內,str 佔用的記憶體便不能被釋放,一直被 str 佔用。

對於垃圾回收機制而言主要是標記清楚和引用計數的方式(具體過程,這裡不深究)。由於垃圾收集器是週期執行的,因此如果為變數分配的記憶體數量很可觀,回收工作量也是相當大的。因此這也是為什麼閉包會消耗大量記憶體的原因進而影響效能的原因。

寫在最後:其實在筆者在不停的逼著自己輸出的時候,就發現很多之前的知識跟現在所寫的知識點串聯起來了。把幾個重要的點串聯起來了,基礎知識的面也就順其自然地形成了,有了這個紮實的知識根基,再去上面做一些拓展工作,也就更加容易了。