閉包(closure)是 Javascript 語言的一個難點,面試時常被問及,也是它的特點,不少高級應用都要依靠閉包實現。本文儘量用簡單易懂的話,講清楚閉包的概念、造成條件及其常見的面試題。本文首發地址爲GitHub博客,寫文章不易,請多多支持與關注!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外部讀取它的內部變量了嗎!面試
咱們能夠對上面代碼進行以下修改:bash
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從新修改,但願對大家有所收穫!