閉包在個人前端學習中一直也是盲點,以前不少次看到別人提到我都是徹底聽不懂。最近一直看書和寫demo,對閉包也逐漸有所理解了,在這裏寫下這篇博客。前端
首先明確幾個概念:
1.JavaScript有函數級做用域,但沒有塊級做用域。
2.當要使用一個變量時,會沿着做用域鏈一步一步向上查找。
這裏有一個demo:es6
var a = 1 function foo () { var a = 2 } foo() console.log(a) // a = 1
結果固然是a = 1
,雖然在foo函數中從新聲明瞭a而且賦給它一個新的值,可是var聲明的變量只在foo()函數中有效,函數執行完畢就會銷燬,所以全局做用域中a的值沒有變化。
接下來再看這個demo:瀏覽器
for (var i = 0; i < 10; i++) { // code } console.log(i) // i = 10
這裏在for循環結束以後仍然能在外部訪問到i,就是由於JavaScript沒有塊級做用域形成的。閉包
閉包的主要特性就是能夠從外部訪問函數內部的屬性和方法。先看一個demo:函數
function foo () { var a = 1 } foo() console.log(a) // 出錯
正常狀況下,定義在函數內部的局部變量在函數執行完以後就會被銷燬,所以在外部是沒法訪問局部變量的。那應該怎麼作才能訪問呢?請繼續看:性能
function foo () { var a = 1 function bar () { console.log(a) } return bar } var baz = foo() baz() // 1
在這個demo中,咱們在函數foo()內部又定義了一個函數bar(),並把它的函數名返回,這樣便能在外部實現對內部變量a的訪問。學習
有人問了:不是說函數執行結束以後內部變量會被銷燬嗎?你這不科學啊。code
是的,原來函數執行結束以後內部變量的確會被銷燬,可是這裏內部函數bar()在foo()執行時被返回並保存到了外部的baz中。這時候foo()執行完後,baz中依舊保存着對函數bar()的引用,所以bar()的做用域並無被釋放,根據以前提到的變量查找方式,在bar()函數的外層做用域中找到了a。ip
以上就是使用閉包能從外部訪問內部屬性的原理。內存
利用閉包強大的特性,最方便的用途就是實現私有變量和私有方法;另外,由於使用閉包會在內存中保存函數做用域,所以也能保存變量的值。
此話怎解?請看下面的demo:
for (var i = 1; i <= 5; i++) { setTimeout(function timer () { console.log(i) }, i * 1000) } // 6 6 6 6 6
這裏的結果並非咱們預想中的每隔一秒依次輸出12345,而是每隔一秒輸出一個6,爲何會這樣呢?
首先,這個6是變量i最終退出循環時的值,其次var聲明的變量沒有塊級做用域,所以i實際上在全局做用域中都訪問獲得。到這裏不難理解了,setTimeout在循環結束後執行timer()函數時,訪問的i實際上在全局做用域中,此時i=6,所以後面的每一個timer()函數執行訪問的i都是6。那麼問題又來了,怎樣才能讓閉包訪問的i是咱們想要的呢?
其實很簡單,讓i變成局部變量,在循環結束以後銷燬,這樣每一個閉包訪問的就是保存在做用域中的局部變量i,也就是對應的12345。請看下面的demo:
for (var i = 1; i <= 5; i++) { (function (j) { setTimeout(function timer () { console.log(j) }, j * 1000) })(i) } // 1 2 3 4 5
這裏用到了當即執行函數(IIFE),可能有些難以理解。那我一步一步解釋:當即執行函數其實就是JavaScript模仿塊級做用域的方法,這裏你能夠把它簡單當作一個函數調用。i就是傳給調用函數的參數,內部匿名函數的形參j接收到i的值開始執行setTimeout。此時j就是函數級做用域中的局部變量,在循環結束後,timer()函數開始執行,由於它是一個閉包,因此能訪問保存在做用域中的變量j,也就輸出了咱們想要的結果。
固然更簡單的方式是用ES6的let來聲明i,這樣也是使得i變成局部變量,其餘關於ES6這裏就很少提,有興趣的能夠自行看let基本用法。
1.由於閉包會使得函數中的變量都保存在內存中,如不能及時釋放會對性能形成影響。
2.在IE9如下的瀏覽器會有內存泄漏的問題。(關於這塊我後續會寫文章詳細說明)
本人經驗尚淺,目前對於前端仍在不斷摸索和學習,文章若有錯誤,歡迎各位指正。最後附上本人博客地址和原文連接,但願能向各位多多學習。