javascript閉包—圍觀大神如何解釋閉包
閉包的概念已經出來很長時間了,網上資源一大把,本著拿來主意的方法來看看。
這一篇文章 學習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.子函數能夠訪問父函數的局部變量,反之則不行。而那個子函數就是閉包!
------有道理,就是上面阮大師反復說明的
好了,就這么多了。
作者:Tyler Ning
出處:http://www.rzrgm.cn/tylerdonet/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,如有問題,請微信聯系冬天里的一把火
浙公網安備 33010602011771號