閉包屬於屬於JavaScript的難點,可是在不少高級應用都須要用到,也是前端面試中常常會考到的點。javascript
談到閉包首先必須瞭解做用域,ES5中,JavaScript的做用域只有兩種,一種是全局做用域,變量在整個程序中一直存在,全部地方均可以讀取;另外一種是函數做用域,變量只在函數內部存在。
JavaScript中變量分爲兩種:全局變量,局部變量。前端
全局變量在程序中的任何一個位置均可以調用,及賦值。java
在函數內部的變量稱之爲局部變量,它能夠在函數內部讀取,在函數外部沒法正常讀取,若是想要讀取函數內部的變量則須要用到閉包。父函數內部定義了子函數,子函數能夠引用父函數做用域中的變量。面試
在網上找了一個圖比較好的解釋了變量與做用域之間的微妙關係。瀏覽器
必須注意的一點的是函數自己的做用域,是定義時的做用域,這裏與this的指向不一樣。閉包
var a = 1; var f = function(){ console.log(a); } function f2(){ var a = 2; f(); } f2() // 1
函數f在全局做用域下定義的,雖然在f2中被引用,可是a仍然是全局做用域下的a。函數
在談到閉包以前,還有一個垃圾回收機制須要瞭解。this
JavaScript的內存生命週期:spa
垃圾回收機制的原理其實很簡單:肯定變量中哪些還在繼續使用的,哪些已經不用的,而後垃圾收集器每隔固定的時間就會清理一下,釋放內存。code
局部變量在程序執行過程當中,會爲局部變量分配相應的空間,而後在函數中使用這些變量,若是函數運行結束了,並且在函數以外沒有仔引用這個變量了,局部變量就沒有存在的價值了,所以會被垃圾回收機制回收。在這種狀況下,很容易辨別,可是並不是全部狀況下都這麼容易。好比說全局變量。在現代瀏覽器中,一般使用標記清除策略來辨別及實現垃圾回收(還有一種叫引用計數,即當變量的引用次數爲零的時候,就表示再也不使用,這裏有個循環計數的bug,現代瀏覽器已經再也不使用它)。
標記清除會給內存中全部的變量都加上標記,而後去掉環境中的變量以及不在環境中可是被環境中變量引用的變量(閉包)的標記。剩下的被標記的就是等到被刪除的變量,緣由是環境中的變量已經沒法訪問到這些變量了。最後垃圾回收器會完成內存清理,銷燬那些被標記的值釋放內存空間。
首先拋出一條閉包的定義:閉包是指這樣的做用域,它包含有一個函數,這個函數能夠調用被這個做用域所封閉的變量、函數或者閉包等內容。
由定義能夠看出:
知足這兩點,均可以叫作閉包。
正常狀況下,一個函數執行完,且沒有在任何地方被調用,這個函數將會被垃圾回收機制銷燬。
舉個例子:
var obj = function () { var a = ''; return { set: function (val) { a = val; }, get: function () { return a; } } }; var b = obj(); b.set('new val'); b.get();
obj這個函數在執行完以後理論上 函數體內的東西都應該被回收掉。但它執行後的返回值 b 具備set和get方法。這兩個方法裏對a保持了引用,因此obj執行過程當中產生的a就不會銷燬。直到b先被回收,這個a纔會回收。
閉包利用的就是以上原理,如下是一個閉包的例子:
function f1() { var a = 1; function f2() { console.log(a); } return f2; } var a = 2; var f = f1(); f() // 1
f執行完以後,其實f指向的f2這個函數在f1這個函數的做用域的引用,也就是說,執行f,至關於在f1函數的做用域這個環境下,執行f2。緣由是f2是在f1中定義的,並且在這個例子中,a的值不會受外界影響。
1.閉包內的變量不會影響到全局變量,也不會被全局變量所影響
2.閉包中被函數引用的局部變量不會被垃圾回收機制回收
3.能夠建立私有變量和私有函數
4.能夠把須要公開的變量和方法綁定在window
上放出來
(function() { // 私有變量 var age = 20; var name = 'Tom'; // 私有方法 function getName() { return `your name is ` + name; } // 共有方法 function getAge() { return age; } // 將引用保存在外部執行環境的變量中,造成閉包,防止該執行環境被垃圾回收 window.getAge = getAge; })();
ES6引入了塊級做用域,主要是let命令以及const命令。他們的特色是不存在變量提高,不能夠重複聲明,只在區塊中有效,存在暫時性死區。
借一個阮一峯老師的暫時性死區的例子:
var tmp = 123; if (true) { tmp = 'abc'; // ReferenceError let tmp; }
在條件語句的區塊中,雖然tmp在賦值後再用let命令聲明,可是let命令已經生效,不歸var所管了。
首先拋磚引玉,來一個關於ES5經典的例子:
var test = function () { var arr = []; for(var i = 0; i < 5; i++){ arr.push(function () { return i*i; }) } return arr; } var test1 = test(); console.log(test1[0]()); console.log(test1[1]()); console.log(test1[2]());
這個例子就不用多講了,最後輸出的值都是25。要注意的有兩點,一個是i的變量提高,一個是i++,i++實際做用位置爲當前循環內容結束,下一個循環以前。i++的意思是當前語句結束後,i加1。當咱們打印i的值的時候,i的循環已經執行完了,i已經變成5了。
當咱們用ES6的時候,狀況就不同了。
var test = function () { const arr = []; for(let i = 0; i < 5; i++){ arr.push(function () { return i*i; }) } return arr; } var test1 = test(); console.log(test1[0]()); console.log(test1[1]()); console.log(test1[2]());
由於使用let,使得for循環爲塊級做用域,let i=0在這個塊級做用域中,而不是在函數做用域中。每次循環都會建立一個新的塊級做用域,i值互相獨立不受影響。因此最後打印的結果是:0,1,4.
var counter = function(){ var count = 1; return function(){ return count++; } }