閉包的概念:
《JavaScript權威指南》:函數對象能夠經過做用域鏈相互關聯起來,函數體內部的變量能夠保存在函數做用域內,這種特性稱爲「閉包」。javascript
很差理解?那就通俗點講:所謂閉包,就是一個函數,這個函數可以訪問其餘函數的做用域中的變量。前端
理解閉包首先要了解嵌套函數的詞法做用域規則,先來看一下這段代碼java
var scope = 'global scope'; // 全局變量 var checkScope = function () { var scope = 'local scope'; // 局部變量 function f() { return scope; } return f(); // => local scope }; checkScope();
checkScope()函數聲明瞭一個局部變量,並定義了一個函數f(),函數f()反回了這個變量的值,最後將函數f()的執行結果返回。你應當很是清楚爲何調用checkscope()函數會返回「local scope」。編程
這個詞法做用域的例子介紹了引擎是如何解析函數嵌套中的變量的。詞法做用域中使用的域,是變量在代碼中聲明的位置所決定的。嵌套的函數能夠訪問在其外部聲明的變量。segmentfault
如今來考慮如下例子 :數組
var scope = 'global scope'; // 全局變量 var checkScope = function () { var scope = 'local scope'; // 局部變量 function f() { return scope; } return f; }; checkScope()(); // 返回值是什麼?
這段代碼中,咱們將函數內的一對圓括號移動到了checkscope()以後。checkscope()如今僅僅返回函數內嵌套的一個函數對象,而不是直接返回結果。在函數做用域外面,調用這個嵌套的函數會發生什麼呢?閉包
這個謎題的答案是,JavaScript中的函數會造成閉包,閉包是由函數以及建立該函數的詞法環境組合而成。也就是說,這個環境包含了這個閉包建立時所能訪問的全部局部變量。架構
在這個例子中,嵌套的函數f()定義在這個做用域鏈裏,其中的變量scope必定是局部變量,無論什麼時候何地執行f(),這種綁定在執行f()時依然有效。所以最後一行代碼返回「local scope」,而不是「global scope「。編程語言
若是你理解了詞法做用域的規則,你就能很容易地理解閉包:函數定義時的做用域鏈到函數執行時依然有效。函數
然而不少同窗以爲閉包很是難理解,由於他們在深刻學習閉包的實現細節時將自已搞得暈頭轉向。他們以爲在外部函數中定義的局部變量在函數返回後就不存在了,那麼嵌套的函數如何能調用不存在的做用域鏈呢?若是你想搞清楚這個問題,你須要更深刻地瞭解相似C語言這種更底層的編程語言,並瞭解基於棧的CPU架構:若是一個函數的局部變量定義在CPU的棧中,那麼當函數返回時它們的確就不存在了。
但回想一下咱們是如何定義做用域鏈的。咱們將做用域鏈描述爲一個對象列表,不是綁定的棧。每次調用JavaScript函數的時候,都會爲之建立一個新的對象用來保存局部變量,把這個對象添加至做用域鏈中。當函數返回的時候,就從做用域鏈中將這個綁定變量的對象刪除。若是不存在嵌套的函數,也沒有其餘引用指向這個綁定對象,它就會被當作垃圾回收掉。若是定義了嵌套的函數,每一個嵌套的函數都各自對應一個做用域鏈,而且這個做用域鏈指向一個變量綁定對象。但若是這些嵌套的函數對象在外部函數中保存下來,那麼它們也會和所指向的變量綁定對象同樣當作垃圾回收。可是若是這個函數定義了嵌套的函數,並將它做爲返回值返回或者存儲在某處的屬性裏,這時就會有一個外部引用指向這個嵌套的函數。它就不會被當作垃圾回收,而且它所指向的變量綁定對象也不會被當作垃圾回收。
下面再來看一個更有意思的示例:— makeAdder函數:
function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12
在這個示例中,咱們定義了makeAdder(x)函數,它接受一個參數x,並返回一個新的函數。返回的函數接受一個參數y,並返回x+y的值。
從本質上講,makeAdder是一個函數工廠 — 他建立了將指定的值和它的參數相加求和的函數。在上面的示例中,咱們使用函數工廠建立了兩個新函數 — 一個將其參數和 5 求和,另外一個和 10 求和。
Add5和add10都是閉包。它們共享相同的函數定義,可是保存了不一樣的詞法環境。在add5的環境中,x爲 5。而在add10中,x則爲 10。
閉包頗有用,由於它容許將函數與其所操做的某些數據(環境)關聯起來。這顯然相似於面向對象編程。在面向對象編程中,對象容許咱們將某些數據(對象的屬性)與一個或者多個方法相關聯。
所以,一般你使用只有一個方法的對象的地方,均可以使用閉包。
接着來看一個uniqueInteger()函數,這個函數使用自身的一個屬性來保存每次返回的值,以便每次都能跟蹤上次返回的值。
var uniqueInteger = (function() { var counter = 0; return function() { return counter++; } })();
你須要仔細閱讀這段代碼才能理解其含義。粗略來看,第一行代碼看起來像將函數賦值給一個變量 uniqueInteger,實際上,這段代碼定義了一個當即調用的函數,所以是這個函數的返回值賦值給變量uniqueInteger。如今,咱們來看函數體,這個函數返回另一個函數,這是一個嵌套的函數,咱們將它賦值給變量uniqueInteger,嵌套的函數是能夠訪問做用域內的變量的,並且能夠訪問外部函數中定義的 counter變量。當外部函數返回以後,其餘任何代碼都沒法訪問 counter變量,只有內部的函數才能訪問到它。
像 counter同樣的私有變量不是隻能用在一個單獨的閉包內,在同一個外部函數內定義的多個嵌套函數也能夠訪問它,這多個嵌套函數都共享一個做用域鏈,看一下這段代碼:
function counter() { var n = 0; return { count: function() { return n++; } reset: function() { n = 0; } }; } var c = counter(), d = counter(); // 建立兩個計數器 c.count(); // =>0 d.count(); // =>0: 它們互不干擾 c.reset(); // reset()和 count()方法共享狀態 c.count(); // =>0: 由於咱們重置了c d.count(); // =>1: 而沒有重置d
counter()函數返回了一個「計數器」對象,這個對象包含兩個方法:count()返回下一個整數,reset()將計數器重置爲內部狀態。首先要理解,這兩個方法均可以訪問私有變量n。再者,每次調用counter()都會建立一個新的做用域鏈和一個新的私有變量。所以,若是調用counter()兩次,則會獲得兩個計數器對象,並且彼此包含不一樣的私有變量,調用其中一個計數器對象的count()或reset()不會影響到另一個對象。
從技術角度看,其實能夠將這個閉包合併爲屬性存取器方法getter和setter。下面這段代碼的私有狀態的實現是利用了閉包,而不是利用普通的對象屬性來實現:
function counter(n) { //函數參數n是一個私有變量 return { //屬性getter方法返回並給私有計數器var遞增1 get count() { return n++; }, //屬性setter不容許n遞減 set count(m) { if (m >= n) n = m; else throw Error("count can only be set to a larger value"); } }; } var c = counter(1000); c.count; // => 1000 c.count; // => 1001 c.count = 2000; c.count; // => 2000 c.count = 2000; // => Error!
須要注意的是,這個版本的counter()函數並未聲明局部變量,而只是使用參數n來保存私有狀態,屬性存取器方法能夠訪問n。這樣的話,調用counter()的 函數就能夠指定私有變量的初始值了。
再來一個例子,用閉包模擬私有方法:
編程語言中,好比 Java,是支持將方法聲明爲私有的,即它們只能被同一個類中的其它方法所調用。
而 JavaScript 沒有這種原生支持,但咱們可使用閉包來模擬私有方法。私有方法不只僅有利於限制對代碼的訪問:還提供了管理全局命名空間的強大能力,避免非核心的方法弄亂了代碼的公共接口部分。
下面的示例展示瞭如何使用閉包來定義公共函數,並令其能夠訪問私有函數和變量。這個方式也稱爲模塊模式(module pattern)
var Counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); console.log(Counter.value()); /* logs 0 */ Counter.increment(); Counter.increment(); console.log(Counter.value()); /* logs 2 */ Counter.decrement(); console.log(Counter.value()); /* logs 1 */
在以前的示例中,每一個閉包都有它本身的詞法環境;而此次咱們只建立了一個詞法環境,爲三個函數所共享:Counter.increment,Counter.decrement和Counter.value。
該共享環境建立於一個當即執行的匿名函數體內。這個環境中包含兩個私有項:名爲privateCounter的變量和名爲changeBy的函數。這兩項都沒法在這個匿名函數外部直接訪問。必須經過匿名函數返回的三個公共函數訪問。
這三個公共函數是共享同一個環境的閉包。多虧 JavaScript 的詞法做用域,它們均可以訪問privateCounter變量和changeBy函數。
咱們已經給出了不少例子,在同一個做用域鏈中定義兩個閉包,這兩個閉包共享一樣的私有變量或變量。這是一種很是重要的技術,但仍是要特別當心那些不但願共享的變量每每不經意間共享給了其餘的閉包,瞭解這一- 點也很重要。看一下下面這段代碼:
//這個函數返回一個老是返回v的函數 function constfunc(v) { return function() { return v; }; } //建立一個數組用來存儲常數函數 var funcs = []; for(var i = 0; i < 10; i++) funcs[i] = constfunc(i); //在第5個位置的元素所表示的函數返回值爲5 funcs[5]() //=> 5
這段代碼利用循環建立了不少個閉包,當寫相似這種代碼的時候每每會犯-一個錯誤:那就是試圖將循環代碼移入定義這個閉包的函數以內,看一下這段代碼:
//返回一個函數組成的數組,它們的返回值是0~9 function constfuncs() { var funcs=[]; for(var i = 0; i < 10; i++) funcs[i] = function() { return i; }; return funcs; } var funcs = constfuncs(); funcs[5]() //返回值是什麼?
上面這段代碼建立了10個閉包,並將它們存儲到一個數組中。這些閉包都是在同一個函數調用中定義的,所以它們能夠共享變量i。當constfuncs()返回時,變量i的值是10,全部的閉包都共享這一個值,所以,數組中的函數的返回值都是同一個值,這不是咱們想要的結果。
關聯到閉包的做用域鏈都是「活動的」,記住這一點很是重要。嵌套的函數不會將做用域內的私有成員複製一份,也不會對所綁定的變量生成靜態快照(staticsnapshot)。
書寫閉包的時候還需注意一件事情, this是JavaScript的關鍵字, 而不是變量。正如以前討論的,每一個函數調用都包含一個thi s值,若是閉包在外部函數裏是沒法訪問this的,除非外部函數將this轉存爲一個變量:
var self = this;
綁定argument的問題與之相似。arguments並非一個關鍵字,但在調用每一個函數時都會自動聲明它,因爲閉包具備本身的綁定的arguments,所以閉包內沒法直接訪問外部函數的參數數組,除非外部函數將參數保存到另一個變量中:
var outerArguments = arguments; // 保存起來以便嵌套的函數能使用它
參考:
* 《JavaScript權威指南》第六版 * [MDN Web 文檔](https://developer.mozilla.org/zh-CN/)
推薦閱讀:
【專題:JavaScript進階之路】
JavaScript之「use strict」
JavaScript之new運算符
JavaScript之call()理解
JavaScript之對象屬性
我是Cloudy,年輕的前端攻城獅一枚,愛專研,愛技術,愛分享。
我的筆記,整理不易,感謝閱讀、點贊和收藏。
文章有任何問題歡迎你們指出,也歡迎你們一塊兒交流前端各類問題!