關於閉包的用法,幾乎是所有前端面試中必點的菜之一,也是考察javascript掌握程度的重要知識之一,下面這題,是某知名IT企業出的題型,我稍加修改,分享如下:
var name = 'global'; var obj = { name : 'obj', dose : function(){ this.name = 'dose'; return function(){ return this.name; } } }
alert(obj.dose().call(this))
請寫出執行結果?
關於這樣的題型,應當怎樣去分析呢?
obj.dose().call(this) 這個表達式有點長,看着有點眼暈,不妨進行一個等價變形。
var xxx = obj.dose(); xxx.call(this);
這樣就清晰多了。這樣一眼就看出是在考察call的用法和this的指向。稍有點基礎的,一眼就可以看出此處的this就是window對象。如果你看不出,就再去看看那本紅寶書3
即使知道此處的this是window還沒完。還要順路普及一下call的用途:
// 1. 替換函數運行環境中的this
// 2. 傳遞參數
// 3. 運行函數
通過前面的分析可以知道xxx是這樣一個函數:
function(){ return this.name; }
由於call指定了this是window,所以return this.name 就是 window中的name,即global;
如果是面試呢,這題到此就結束了,不過舉一反三才是我的目的。因此,下面我稍改一下題目:
var name = 'global'; var obj = { name : 'obj', dose : function(){ this.name = 'dose'; return function(){ return this.name; }.bind(this) } } alert(obj.dose().call(this))
由於return的function中用了bind,所以相當於固定了this,外邊再call什么進來,也只是礙眼法而已。由於函數內部邦定了this,所以此處的情況要另外分析了
首先,obj對象定義了name屬性為'obj';接着在dose 方法內,又改寫了name屬性為'dose'; 根據作用域鏈的就近原則,alert訪問的肯定就是'dose'這個值了。
然而ie派認為在return中用bind不常見,兼容性也不高。那不妨再變一下:
var name = 'global'; var obj = { name : 'obj', dose : function(){ var that = this; this.name = 'dose'; return function(){ return that.name; } } } alert(obj.dose().call(this))
這種寫法,自然大家都比較認同了。考察還是相同的內容,只不過是邦定this的手法不一樣而已。與其說是考察閉包,不如說是考察對基礎知識的理解,因為bind,call,apply之類的方法都是平時使用頻率很高的,對它們多花點時間琢磨一下,必然是有好處的。
最后呢再分享一個面試的趣事。面試官問我,用閉包有什么好處?
1. 延長作用域鏈。
這一點大家是熟知的,因為閉包函數可以訪問外層函數作用域中的變量及對象,以代碼來演示一下:
function wrap () { var out = '外部變量'; return function (){ //這里可以訪問外部函數中的變量 //實際上就是創建了一個閉包函數 alert(out); } } var inner = wrap(); //雖然wrap運行完畢了,但是inner依然可以訪問它所創建的作用域中的變量 //這就是閉包第一個用法 inner();
2. 生成預編譯函數。
這一點是借用jquery中的說法,實際上就是通過閉包把外層函數提供的參數保存起來,在閉包運行的時候就可以得到預先指定的參數
var fn = []; for(var i = 0;i<3;i++){ (function(n){ fn.push(function(){ alert(n) }) })(i) }
說化函數的curry化,可能知道的人會更多一點,和上面的例子相似,不過它強調的是參數的積累。
function addGenerator(num){ return function(toAdd){ return num + toAdd; }; } //創建一個新的函數 var addFive = addGenerator(5); alert(addFive(4)==9) //true
3.更好的組織代碼,比如模塊化,異步代碼轉同步等。
Deferred.next(function(){ alert(1) return Deferred.wait(3) }).next(function(){ alert(2) }).next(function(){ alert(3); });
4. 處理異步造成的變量不能即時傳遞的問題
/** * html結構: * <ul> * <li> 0</li> * ...... * <li> 9 </li> * </ul> */ //點擊彈出對應的數字 var items = document.querySelectorAll('li'); for(var i=0;i<items.length;i++){ items[i].onclick = function(){ alert(i) } } //上面的程序結果是:每次都彈出10; //為了在用戶點擊的時候,能彈出對應的數字 // 需要構建一個閉包,將參數緩存起來 for(var i=0;i<items.length;i++){ items[i].onclick = (function(n){ return function(){ alert(n) } })(i) } // 這時點擊的時候就會彈出邦定的數字了,強烈推薦試一下
暫時就想到這些,其它的想到再補充。
他接着又問我,那用閉包又有什么壞處?
1. 增加了內存的消耗。
2. 某些瀏覽器上因為回收機制的問題,有內存溢出風險。
3. 增加了代碼的復雜度,維護和調試不便。
我balabala說了一些,他就笑了。說你這一邊是矛,一邊是盾,到底是矛好呢還是盾好呢?當時也沒想這么多,反射性的回答說,看情況選用咯。他說這樣是不行的。
原來挖了個坑在這里等着我呢,真是太不厚道了。凡事都有兩面性嘛,只要撐握的好,自然是可以避害用利。我們都知道電是很危險,也很有用,只要掌握了它的特性,就能很好的利用,而不是受其害,所以並不是矛盾的就不可取。拿最后一個例子來說,如果不用閉包,難道就沒有辦法了嗎?顯然不是的,比如下面的代碼就不用閉包,照樣解決該問題:
function fn(n){ items[n].onclick = function(){ alert(n) } } for(var i=0;i<items.length;i++){ fn(i) }
所以,不要認為閉包有缺點就不敢用,也不要因為閉包的優點而濫用。 是否選擇閉包,視我們的需要來定。
總結一點:學東西不可淺嘗則止,一定要深入原理,舉一反三,利用它的優點,規避它的缺點。