閉包的概念已經出來很長時間了,網上資源一大把,本著拿來主意的方法來看看。
這一篇文章 學習Javascript閉包(Closure) 是大神阮一峰的博文,作者循序漸進,講的很透徹。下面一一剖析。
1.變數的作用域
變數的作用域有區域性和全域性兩種,在javascript的函式內部可以訪問全域性變數,如下:
// 函式內部可以直接讀取全域性變數 var n = 99; function f1() { alert(n); } f1();
在f1函式中可以訪問到全域性變數n。輸出如下:
反過來就不行了,在函式外部不能讀取函式內部的變數,例如這樣:
//函式外部無法讀取函式內部的區域性變數,這裡會報錯 function f2() { var m = 99; } alert(m) //報錯
javascript還有一個比較特殊的地方,在函式內部如果沒有使用var,const,let修飾符宣告變數,那麼這個變數不再是區域性變數而是一個全域性變數,如下:
function f2() { m = 99; } f2(); alert(m)
輸出99:
是不是很神奇,但是這個經常給人造成困惑。
2.如何從外部讀取函式內部的區域性變數
很多場合下要訪問函式內部的區域性變數,變通的方式是在函式內部定義函式,如下:
function f3() { var a = 999; function f4() { alert(a); } return f4 } var result = f3(); result();
函式f4包含在函式f3裡面,所以f4範圍內可以訪問到f3中的那個變數a,反過來是不行的,Javascript語言特有的"鏈式作用域"結構(chain scope),子物件會一級一級地向上尋找所有父物件的變數。所以,父物件的所有變數,對子物件都是可見的,反之則不成立。
3.閉包的概念
可以簡單理解成定義在函式內部的函式,這個內部函式可以把內部函式作用域內的變數傳播到外面。由於在javascript中只有內部的子函式才能讀取區域性變數,因此可以把閉包簡單的理解成“定義在一個函式內部的函式”。本質上,閉包就是將函式內部和函式外部連線起來的橋樑。
4.閉包的作用
閉包的第一個用處就是讀取函式內部的變數,另一個作用就是讓這些變數始終儲存在記憶體中。來看下面的程式碼:
function f5() { var b = 111; nAdd = function () { b += 1; } function f6() { alert(b); } return f6 } var result1 = f5(); result1(); nAdd(); result1();
上面程式碼兩次彈出框,第一次是輸出111,第二次是112,這就證明函式函式f5內的區域性變數b一直儲存在記憶體中,並沒有在f5呼叫後被自動清除。
這就說明,第一次呼叫result1();的時候給變數b賦值了,然後呼叫全域性函式nAdd的時候變數b仍然還在記憶體中,給他加1就變成112了。原因就在於f5是f6的父函式,而f6被賦給了一個全域性變數,這導致f6始終在記憶體中,而f6的存在依賴於f5,因此f5也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制(garbage collection)回收。
這段程式碼中另一個值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關鍵字,因此nAdd是一個全域性變數,而不是區域性變數。其次,nAdd的值是一個匿名函式(anonymous function),而這個匿名函式本身也是一個閉包,所以nAdd相當於是一個setter,可以在函式外部對函式內部的區域性變數進行操作。
5.注意
1)由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。解決方法是,在退出函式之前,將不使用的區域性變數全部刪除。其實一般不會這樣用!!!
2)閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(object)使用,把閉包當作它的公用方法(Public Method),把內部變數當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函式內部變數的值。其實一般不會這樣用!!!
最後作者給出了思考題目,如下:
var name = 'The Window'; var object = { name: 'My Object', getNameFunc: function () { // 這裡的this是函式 console.info(this) return function () { //匿名函式的執行環境是windows console.info(this) return this.name; } } }; alert(object.getNameFunc()());
其實從變數的值已經看到答案了,在物件object內部的函式getNameFunc裡面返回了一個匿名函式這個匿名函式的作用域是window,所以這裡輸出的是'The Window',如下:
我在日誌裡面加上的除錯語句可以看出端倪:
看下面的程式碼:
var name1 = "The Window"; var object1 = { name1: 'My Object1', getNameFunc:function () { var that = this; return function () { return that.name1 } } } alert(object1.getNameFunc()());
這裡在函式內部使用var that = this語句先把當前上下文的物件儲存下來,在匿名函式中使用that.name1,這樣就是當前物件中的name1,於是輸出了“MyObject1”。其實可以用es6中的箭頭函式,如下:
var name1 = "The Window"; var object1 = { name1: 'My Object1', getNameFunc:function () { return () => { return this.name1 } } } alert(object1.getNameFunc()());
箭頭函式會繫結object1的作用域,於是仍任是object1的屬性name1,和上面輸出的結果一樣。
6.深入的理解
在知乎上也有人在討論這個問題,知乎你懂的,比較嚴謹,如何通俗易懂的解釋javascript裡面的‘閉包’?這一篇問答裡有人給出了其他的解釋。
1.每次定義一個函式,都會產生一個作用域鏈(scope chain)。當JavaScript尋找變數varible時(這個過程稱為變數解析),總會優先在當前作用域鏈的第一個物件中查詢屬性varible ,如果找到,則直接使用這個屬性;否則,繼續查詢下一個物件的是否存在這個屬性;這個過程會持續直至找到這個屬性或者最終未找到引發錯誤為止。
------有道理,關鍵是弄懂這個作用域鏈,說白了就是花括號的層級及各種函式的的上下文作用域,比如在函式中定義變數不用var,let它居然是全域性的,這個是javascript比較特殊的地方,強型別語言估計早就報錯了。
2.JavaScript中的函式執行在它們被定義的作用域裡,而不是它們被執行的作用域裡。
------有道理,上面的最後一個程式碼段中的例子,本來執行object1.getNameFunc()()這一句的時候,執行作用域中的name1是var name1 = "The Window";這個,但是彈出來的確實定義getNameFunc這個函式的作用域內的name1: 'My Object1',
3.子函式能夠訪問父函式的區域性變數,反之則不行。而那個子函式就是閉包!
------有道理,就是上面阮大師反覆說明的
好了,就這麼多了。