這一章我們介紹函數表達式,在開始的時候我們會複習到很多之前學過的知識。
定義
我們之前學過函數提升相關的知識,定義的函數會提升,定義的函數作爲變量則不會提升,我們還舉例說過下面的代碼才能達到效果
var condition = false;
var sayHi = null;
if(condition){
sayHi = function(){
alert('true');
}
}else{
sayHi = function(){
alert('false');
}
}
現在,我們給這種函數定義之後賦值給變量的寫法叫函數表達式。
var func = function(arg1, arg2){
return arg1 + arg2;
}
這種寫法是函數表達式最常見的寫法,但不是唯一的寫法,我們之後還會介紹別的函數表達式的寫法。
遞歸
我們知道遞歸就是自己調用自己,我們也學習了下面這種寫法不好,最好使用arguements.callee()方法以防止函數名稱的改變:
function A(){
A();
}
var B = A;
A = null;
B(); // 報錯,這種方法不好,在函數中調用了調用了函數名造成了耦合
function A(){
arguements.callee();
}
var B = A;
A = null;
B(); // 可以運行,但是嚴格模式不允許使用callee
我們發現這兩種寫法一種不好,好的方法嚴格模式還不讓用,這怎麼辦呢?我們使用下面的辦法:
var func = (function A(){
A();
}); // 這裏的括號寫不寫都可以,但是寫上更好
var B = func;
func = null;
B();
閉包的概念
閉包是有權訪問另一個函數作用域中的變量的函數。
閉包的調用原理
我們之前在函數的學習中,學過函數的調用原理,如果不記得了可以複習一下,我們這裏看看閉包的調用原理
function createComparisonFunction(propertyName){
return function(obj1, obj2){
var value1 = obj1[propertyName];
var value2 = obj2[propertyName];
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
};
}
var compare = createComparisonFunction('name');
var result = compare({name : 'Nic'}, {name : 'Bob'});
alert(result);
compare = null;
我們看到實現的效果是createComparisonFunction執行完畢之後,還可以調用compare,compare會記住之前初始化的name值。我們說,一般在函數執行完畢之後,其活動對象就會被銷燬,propertyName只是一個局部變量,爲什麼一直存在了呢?這就是閉包跟普通函數調用的不同。
下面我們來描述一下上面代碼中函數的調用:
- 定義了compare,調用了createComparisonFunction,創建了createComparisonFunction的執行環境,以及作用域鏈,創建了createComparisonFunction函數的活動對象,作用域鏈上有兩個對象,一個createComparisonFunction的活動對象,一個全局活動對象。createComparisonFunction的活動對象中propertyName的值爲name
- 在createComparisonFunction函數調用完成之後,其執行環境,作用域鏈都被銷燬了,但是由於其返回了一個函數,這個函數的作用域鏈中引用了createComparisonFunction的活動對象,所以其活動對象無法被銷燬。
- 當調用compare方法的時候,使用了之前遺留的活動對象,所以name生效了
- 在使用完成之後,調用compare = null釋放compare對象,才能徹底釋放遺留的活動對象
其具體的指向如下圖:
注意:
書中寫的是作用域鏈是在函數調用的時候生成的,按照上面的實驗,createComparisonFunction只是返回了函數,其活動對象就無法被銷燬了,這是爲什麼呢?
因爲其定義使用的是函數表達式而非函數定義,在使用函數表達式創建函數的時候,因爲變量名必須有所指向,所以作用域鏈,活動對象就都創建出來了。
閉包中的變量問題
我們看下面的代碼返回什麼:
function createFunctions(){
var result = new Array();
for(var i = 0;i < 10; i++){
result[i] = function(){
return i;
}
}
return result;
}
for(var i = 0;i < 10; i++){
alert(createFunctions()[i]()); // 全是10
}
分析一下:
- createFunctions返回的是一個函數數組,返回的是一堆函數,但是這一堆函數依賴着同一個createFunctions的活動對象,而i存在於createFunctions的活動對象中,並不存在於每一個返回的函數的活動對象中
- 當調用createFunctions()的時候,result數組就被填充完了,填充的過程中,i不斷地變大,直到10。所以不管createFunctions()i中的i是幾,其返回的值都是10
那麼如何返回正確的值呢。其關鍵在如何讓函數返回的i的值存在於各自的活動對象中而不是createFunctions的活動對象中。我們可以使用下面的方法:
function createFunctions(){
var result = new Array();
for(var i = 0;i < 10; i++){
result[i] = function(num){
return num;
}(i);
}
return result;
}
for(var i = 0;i < 10; i++){
alert(createFunctions()[i]); // 返回0-10
}
結合上面說的問題,這種方法從根本上解決問題,根本問題是變量在哪個活動對象的問題,通過函數的賦值,我們將i作爲變量傳給了內部函數的活動對象,由於i是基本類型,傳值,所以i被傳進了內部函數的活動對象中。
閉包中的this對象
由於活動對象中的this指向的是調用的環境對象,所以調用閉包的函數,this的指向問題很大。我們分析下面的例子:
var name = 'The Window';
var object = {
name: 'My Object',
getNameFunc: function(){
return function(){
return this.name;
}
}
};
alert(object.getNameFunc()()); // The Window
分析:
this返回的是調用的對象,因爲這裏object.getNameFunc()獲取了內部方法,再加一個(),等於在全局對象上調用了方法,所以指向了全局對象window
而實際上,我們認爲內部函數是在object中被調用的,我們希望this指向object,爲了實現這種情況,我們可以使用that
var name = 'The Window';
var object = {
name: 'My Object',
getNameFunc: function(){
var that = this;
return function(){
return that.name;
}
}
};
alert(object.getNameFunc()()); // My Object
分析一下:
- 在閉包中使用that,其活動對象中沒有that,所以到getNameFunc的活動對象中找這個值,找到了之後使用getNameFunc的this屬性作爲that
- 如果想在閉包中訪問getNameFunc的arguements對象,也需要這樣操作
閉包的內存泄漏問題
IE9之前如果閉包中存着一個HTML元素,那麼這個元素不能被銷燬。在IE9之後和其他瀏覽器中沒有問題,我們在這裏不展開說
閉包的用處:模仿塊作用域
在實際的開發中,所有的程序員都在同一個全局作用域下編程,很容易帶來潛在的命名衝突,我們應該儘量少的在全局作用域中添加變量,再者,在全局作用域下添加的閉包如果不即時釋放,會消耗額外的資源。
我們知道其他語言有塊作用域而js沒有,其他的語言在for循環的時候定義了i,for循環結束,i就失效了,而js不會。我們還知道,函數是js中唯一可以實現塊作用域的方法,一個函數中的作用域纔是自己的。這樣我們就有了下面的方法來解決問題,模擬塊作用域。
(function() {
// 這是一個模擬的塊作用域
// 這是一個閉包
})();
經過這麼一寫,包起來的就是自己的作用域,在調用完成之後立即釋放。那麼這個是不是一個閉包呢?我們學過,閉包是有權訪問另一個函數作用域變量的函數,我們看,把這段代碼放到任何函數中去,都可以訪問這個函數的作用域變量,所以這是一個閉包
我們來看幾個應用
function outputNumber(count) {
(function() {
for (var i = 0; i < count; i++) {
alert(i);
}
})();
alert(i); //報錯 i is not defined
}
outputNumber(3);
使用這種方法可以:
- 限制像全局作用域添加過多的變量和函數,減少命名衝突
- 創建私有作用域,開發人員可以使用自己的變量不擔心搞亂全局作用域
在ES6的let出現之後我們不需要這麼模擬了,可以使用真正的塊變量:
{
let a = 1;
}
閉包的用處:私有變量
function MyObj(name) {
this.getName = function() {
return name;
};
this.setName = function(value) {
name = value;
};
}
var person = new MyObj('wf');
alert(person.getName()); //wf
person.setName('s');
alert(person.getName()); //s
通過對於構造函數中添加get set方法,實現了對於name屬性的封裝。這個實踐實現了私有變量但是這個模式基於構造函數模式,我們之前論證過構造函數模式並不是很好的實現對象的模式,每個實例都會重建get set方法,有額外的資源消耗。所以這個方式並不是很好的方式。
閉包的用處:靜態私有變量
我們之前說了不想每次實例化都重建方法,所以我們很自然的想到之前的辦法,將set get方法寫到構造函數的prototype中,寫道構造函數之外。
我們學了閉包,將這一系列操作寫入一個閉包中只暴露構造函數不失爲一個好辦法。
(function() {
var name = ''; //這是一個私有變量
Person = function(value) {
name = value;
};
Person.prototype.getName = function() {
return name;
};
Person.prototype.setName = function(value) {
name = value;
};
})();
var person1 = new Person('Nic');
alert(person1.getName()); //Nic
閉包的用處:模塊模式
模塊模式就是一種爲單例創建私有變量和特權方法。這種方法用於對全局單例對象定義私有變量,並且定義方法操作時候使用
var application = function() {
var component = new Array(); //定義
component.push(new BaseComponent()); // 初始化
return {
//返回單例對象
getComponent: function() {
return component.length;
},
registerComponent: function(com) {
if (typeof com == 'object') {
component.push(com);
}
}
};
}();
閉包的用處:增強模塊模式
我們看到之前我們講的模塊模式返回的是一個字面量標識的對象,這個對象使用typeof判斷就是個Object,如果我們要求,返回的單例必須是某類型的,這時候我們可以把字面量改爲特定類型的實例,也不影響功能,看下面的的代碼:
var application = (function() {
var component = new Array(); //定義
var app = new BaseComponent(); // 要求返回的必須是BaseComponent類型
app.getComponent = function() {
return component.length;
};
app.registerComponent = function(com) {
if (typeof com == 'object') {
component.push(com);
}
return app;
};
})();