JavaScript之閉包的實現、閉包中的this物件
閉包
函式物件可以通過作用域鏈關聯起來,函式體內的變數可以儲存在作用域中,這種特性稱“閉包”。要理解閉包,首先要理解巢狀函式的詞法作用域規則:先看下列一段程式碼:
var a = "Tom"; //全域性變數
function curr () {
var a = "Bob"; //區域性變數
function () {
return a;
}
return f(); //將f函式執行的結果返回 Bob
}
curr();
當curr()函式呼叫執行完畢後,返回的是f()函式執行的結果即”Bob“,當將這段程式碼修改之後:
var a = "Tom"; function curr () { var a = "Bob"; function f () { return a; } return f; //返回的是這個f函式物件 } curr()(); //f()
當curr()函式執行完畢後,返回的是f函式物件,後面再跟一個圓括號,就是呼叫f函式。定義curr函式時,建立了一個作用域鏈,呼叫curr函式時,會新建立一個物件用來儲存這個函式的區域性變數和引數,並把這個物件儲存在這個作用域上,函式f也定義在了這個作用域鏈中。當curr()函式執行後,其作用域鏈依然有效,上面還儲存著其區域性變數a,巢狀函式f也還在了這個作用域鏈上,因此此時區域性變數a和f函式還是繫結在一起的。最後執行f函式的結果是"Bob"(引用的變數a就是這個區域性變數)。
詞法作用域規則:JavaScript函式執行的時候用到了作用域鏈,這個作用域鏈在定義函式的時候建立的,巢狀的f函式定義在這個作用域鏈中,其中變數a是區域性變數,無論何時何地執行f函式,這種繫結在執行f時依然有效。
實現閉包:
要理解閉包,首先理解詞法作用域鏈規則:函式定義時所建立的作用域鏈到函式執行時依然有效。
首先回顧一下作用域鏈:定義函式時就會建立作用域鏈,將作用域鏈看作是物件列表或鏈列表。每次呼叫函式時,就會建立一個新物件用來儲存(定義)區域性變數,並把這個物件新增到作用域鏈中。當函式返回時,就將這個物件從作用域鏈中刪除:
1、如果這個函式沒有定義巢狀的函式,也沒有其它引用指向這個物件,那麼這個物件就會被回收掉。2、如果這個函式定義了巢狀的函式,每個巢狀的函式都有自己的作用域鏈,並且這個作用域鏈指向一個變數繫結物件:
- a、如果這些巢狀的函式在函式外部保留了下來,那麼它們也會和所指向的變數繫結物件被當作垃圾回收掉。
- b、如果 這個巢狀的函式物件 被當作返回值返回或作為某物件的屬性儲存了下來,這時就會有一個外部引用指向這個巢狀的函式物件,它就不會被當作垃圾回收掉,並且它所指向的變數繫結物件也不會被回收掉。
下面舉個經典例子:返回一個函式組成的陣列,它們返回0~9的數
functino consFunc () {
var arr = [];
for (var i = 0; i < 10; i ++) {
arr[i] = function () {
return i;
};
}
return arr;
}
var a = consFunc(); //呼叫並返回consFunc
console.log(a[5]()); //10 看來沒有返回我們想要的值
上面程式碼建立了10個閉包,並將它們儲存在一個數組中,因為每個閉包都是在同一個函式 呼叫中 定義的,因此它們都引用同一個區域性變數i。當consFunc返回時,變數i的值為10,再使用a[5]()呼叫閉包函式,此時所有閉包函式都引用同一個變數i,但此時的變數i的卻是10,所以陣列中的函式的返回值是同一個值。這不是我們想要的。巢狀的函式不會將作用域中的私有變數複製一份的。解決方案1:可以建立一個匿名函式讓閉包來”記住“變數i的不同值。
function consFun () {
var arr = [];
for (var i = 0; i < 10; i ++) {
arr[i] = function (num) {
return function () {
return num;
};
}(i);
}
return arr;
}
var a = consFunc();
console.log(a[5]()); //5
在重寫了consFunc函式後,每個函式都會返回各自不同的索引值了。這裡定義了一個匿名函式,並將立即執行這個匿名的結果賦值給陣列。這裡的匿名函式有個引數num,也就是最終要返回的值。在呼叫每個匿名函式(閉包)時,傳入了變數i。由於函式是按值傳遞的,所以就會將變數i的當前值複製給num,在這個閉包函式內部,又建立一個閉包函式作為返回值,於是陣列中存放的是這個返回值,這個返回值是一個函式物件,arr陣列中的每個函式都有自己的num變數的副本,因此可以返回不同的值。
解決方案2:其實可以將上面程式碼修改為更簡潔的方式:
function consFun () {
var arr = [];
for (var i = 0; i < 10; i ++) {
arr[i] = function () { //呼叫匿名函式自身,返回的是值。
return i;
}();
}
return arr;
}
var a = consFunc();
console.log(a[5]); //5
當consFunc函式返回時,陣列中的閉包函式已經執行完畢,並將執行的結果儲存在了陣列中。
在閉包中關於this物件
我們知道,this物件是在執行時基於函式的執行環境繫結的:在全域性環境中,this物件就是window物件。而當函式作為某個物件的方法呼叫時,this物件就指向這個物件。匿名函式具有全域性性,this物件通常指向window物件,不過有時候,由於閉包函式書寫方式不同,就沒有那麼明顯。
特別說明:當函式被呼叫時會自動取得兩個屬性:this物件和arguments物件。
var name = "hello window";
var obj = {
name : "hello obj",
getName : function () {
return function () {
return this.name;
}
}
};
console.log(obj.getName()()); //hello window
上例定義了一個全域性變數name和一個物件obj。當obj.getName()方法,返回的是一個匿名函式物件,而匿名函式又返回"this.name",由於getName返回的是一個函式,因此"obj.getName()()"就會立即呼叫getName返回的匿名函式,此時匿名函式是作為全域性物件window的方法呼叫的,因此匿名函式中的this物件指向window物件,以"this.name"就相當於"window.name",所以最後返回的結果是"hello window"。
上述例子在開始建立前,想要的是匿名函式返回obj物件中的name屬性。但匿名函式中的this物件是指向window物件的,無法直接引用到obj物件。我們知道在"obj.getName()"中,getName函式是作為obj物件的方法被呼叫的,那麼其this物件就指向了obj物件,那我們可不可以這樣想:如果能使匿名函式(內部函式)引用到getName函式的this物件的話,那麼間接地不就引用到了obj物件了麼,匿名函式最後返回的不就是想要的"hello obj"麼。
我們知道,函式在被呼叫時會自動新增兩個屬性:this物件和arguments物件。內部函式在搜尋這兩個變數時,只會搜尋到其活動物件為止,因此不可能直接訪問到外部函式的這兩個變數。如果將外部的this物件儲存在一個閉包函式(內部函式)可以訪問到的變數裡,是不是可以說這個閉包函式就可以訪問到外部函式的this物件呢?
var name = "hello window";
var obj = {
name : "hello obj",
getName : function () {
var that = this; //將getName函式的this物件儲存在一個變數裡。
return function () {
return that.name; //由閉包函式去訪問這個變數。
}
}
};
console.log(obj.getName()()); //hello obj
這樣,將getName函式的this物件儲存在一個變數裡,而這個變數可被閉包函式訪問,當閉包函式搜尋that.name時,就會搜尋到getName函式的this.name,getName函式的this物件是引用到obj物件的(that就相當於此this),即使這個函式返回後,that也是引用到obj物件的,那麼最後閉包函式返回的就是"hello obj"。
注意:閉包函式中的that引用的是obj物件,但其this物件引用的是window物件。
var name = "hello window";
var obj = {
name : "hello obj",
getName : function () {
var that = this; //將getName函式的this物件儲存在一個變數裡。
return function () {
return that.name + "-" + this.name; // 此處的this物件是指向window物件的。
}
}
};
console.log(obj.getName()()); //hello obj - hello window
也就是說,儘管這樣修改程式碼,這個閉包函式還是作為window物件的方法呼叫的。