閉包(closure)是 Javascript 語言的一個難點,面試時常被問及,也是它的特點,不少高級應用都要依靠閉包實現。本文儘量用簡單易懂的話,講清楚閉包的概念、造成條件及其常見的面試題。javascript
咱們先來看一個例子:html
var n = 999; function f1() { console.log(n); } f1() // 999
上面代碼中,函數f1能夠讀取全局變量n。可是,函數外部沒法讀取函數內部聲明的變量。java
function f1() { var n = 999; } console.log(n) // Uncaught ReferenceError: n is not defined
上面代碼中,函數f1內部聲明的變量n,函數外是沒法讀取的。git
若是有時須要獲得函數內的局部變量。正常狀況下,這是辦不到的,只有經過變通方法才能實現。那就是在函數的內部,再定義一個函數。github
function f1() { var n = 999; function f2() { console.log(n); // 999 } }
上面代碼中,函數f2就在函數f1內部,這時f1內部的全部局部變量,對f2都是可見的。既然f2能夠讀取f1的局部變量,那麼只要把f2做爲返回值,咱們不就能夠在f1外部讀取它的內部變量了嗎!面試
咱們能夠對上面代碼進行以下修改:閉包
function f1(){ var a = 999; function f2(){ console.log(a); } return f2; // f1返回了f2的引用 } var result = f1(); // result就是f2函數了 result(); // 執行result,全局做用域下沒有a的定義, //可是函數閉包,可以把定義函數的時候的做用域一塊兒記住,輸出999
上面代碼中,函數f1的返回值就是函數f2,因爲f2能夠讀取f1的內部變量,因此就能夠在外部得到f1的內部變量了。函數
閉包就是函數f2,即可以讀取其餘函數內部變量的函數。因爲在JavaScript語言中,只有函數內部的子函數才能讀取內部變量,所以能夠把閉包簡單理解成「定義在一個函數內部的函數」。閉包最大的特色,就是它能夠「記住」誕生的環境,好比f2記住了它誕生的環境f1,因此從f2能夠獲得f1的內部變量。在本質上,閉包就是將函數內部和函數外部鏈接起來的一座橋樑。性能
當函數能夠記住並訪問所在的詞法做用域,即便函數是在當前詞法做用域以外執行,這就產生了閉包。 ----《你不知道的Javascript上卷》測試
我我的理解,閉包就是函數中的函數(其餘語言不能函數再套函數),裏面的函數能夠訪問外面函數的變量,外面的變量的是這個內部函數的一部分。
每一個函數都是閉包,每一個函數天生都可以記憶本身定義時所處的做用域環境。把一個函數從它定義的那個做用域,挪走,運行。這個函數竟然可以記憶住定義時的那個做用域。無論函數走到哪裏,定義時的做用域就帶到了哪裏。接下來咱們用兩個例子來講明這個問題:
//例題1 var inner; function outer(){ var a=250; inner=function(){ alert(a);//這個函數雖然在外面執行,但可以記憶住定義時的那個做用域,a是250 } } outer(); var a=300; inner();//一個函數在執行的時候,找閉包裏面的變量,不會理會當前做用域。
//例題2 function outer(x){ function inner(y){ console.log(x+y); } return inner; } var inn=outer(3);//數字3傳入outer函數後,inner函數中x便會記住這個值 inn(5);//當inner函數再傳入5的時候,只會對y賦值,因此最後彈出8
棧內存提供一個執行環境,即做用域,包括全局做用域和私有做用域,那他們何時釋放內存的?
通常狀況下,函數執行會造成一個新的私有的做用域,當私有做用域中的代碼執行完成後,咱們當前做用域都會主動的進行釋放和銷燬。但當遇到函數執行返回了一個引用數據類型的值,而且在函數的外面被一個其餘的東西給接收了,這種狀況下通常造成的私有做用域都不會銷燬。
以下面這種狀況:
function fn(){ var num=100; return function(){ } } var f=fn();//fn執行造成的這個私有的做用域就不能再銷燬了
也就是像上面這段代碼,fn函數內部的私有做用域會被一直佔用的,發生了內存泄漏。所謂內存泄漏指任何對象在您再也不擁有或須要它以後仍然存在。閉包不能濫用,不然會致使內存泄露,影響網頁的性能。閉包使用完了後,要當即釋放資源,將引用變量指向null。
接下來咱們看下有關於內存泄漏的一道經典面試題:
function outer(){ var num=0;//內部變量 return function add(){//經過return返回add函數,就能夠在outer函數外訪問了 num++;//內部函數有引用,做爲add函數的一部分了 console.log(num); }; } var func1=outer(); func1();//其實是調用add函數, 輸出1 func1();//輸出2 由於outer函數內部的私有做用域會一直被佔用 var func2=outer(); func2();// 輸出1 每次從新引用函數的時候,閉包是全新的。 func2();// 輸出2
1.能夠讀取函數內部的變量。
2.可使變量的值長期保存在內存中,生命週期比較長。所以不能濫用閉包,不然會形成網頁的性能問題
3.能夠用來實現JS模塊。
JS模塊:具備特定功能的js文件,將全部的數據和功能都封裝在一個函數內部(私有的),只向外暴露一個包信n個方法的對象或函數,模塊的使用者,只須要經過模塊暴露的對象調用方法來實現對應的功能。
具體請看下面的例子:
//index.html文件 <script type="text/javascript" src="myModule.js"></script> <script type="text/javascript"> myModule2.doSomething() myModule2.doOtherthing() </script>
//myModule.js文件 (function () { var msg = 'Beijing'//私有數據 //操做數據的函數 function doSomething() { console.log('doSomething() '+msg.toUpperCase()) } function doOtherthing () { console.log('doOtherthing() '+msg.toLowerCase()) } //向外暴露對象(給外部使用的兩個方法) window.myModule2 = { doSomething: doSomething, doOtherthing: doOtherthing } })()
咱們要實現這樣的一個需求: 點擊某個按鈕, 提示"點擊的是第n個按鈕",此處咱們先不用事件代理:
..... <button>測試1</button> <button>測試2</button> <button>測試3</button> <script type="text/javascript"> var btns = document.getElementsByTagName('button') for (var i = 0; i < btns.length; i++) { btns[i].onclick = function () { console.log('第' + (i + 1) + '個') } } </script>
萬萬沒想到,點擊任意一個按鈕,後臺都是彈出「第四個」,這是由於i是全局變量,執行到點擊事件時,此時i的值爲3。那該如何修改,最簡單的是用let聲明i
for (let i = 0; i < btns.length; i++) { btns[i].onclick = function () { console.log('第' + (i + 1) + '個') } }
另外咱們能夠經過閉包的方式來修改:
for (var i = 0; i < btns.length; i++) { (function (j) { btns[j].onclick = function (i) { console.log('第' + (i + 1) + '個') } })(i) }
若是以爲文章對你有些許幫助,歡迎在個人GitHub博客點贊和關注,感激涕零!
ps:文章於2018.11.16從新修改,但願對大家有所收穫!