JavaScript之閉包
第一部分:基本概念
我們知道根據作用域鏈的規則,一個函數是不能訪問到在與他同一個作用域內的函數內的內部變量的,如下所示:
function foo() { var a = 5; console.log(a); } foo(); function bar() { var b = a + 5; console.log(b); } bar();
在執行到第二個函數時,由於找不到a,所以會報錯: a is not defined。
但是有的同學說了,我們可以將a作為全局變量啊,沒錯,這樣一定可以做到, 但是全局變量是由汙染的,一般我們最好定義為局部變量, 廢話不多說,看看閉包怎麽實現:
function foo() { var a = 5; console.log(a); function closure() { return a; } return closure; } var foo_bar = foo(); function bar() { var a = foo_bar() var b = a + 5; console.log(b); // 10 } bar();
這段代碼同樣執行了相同的任務,並且還成功的拿到了foo內部的局部變量。 雖然foo()函數之前已經執行結束了,但是因為閉包closure還沒有執行,它保留著對a的引用,所以在foo函數執行完了之後a並不會被銷毀。
這就是閉包。
在JavaScript中,閉包恐怕是很多人不能理解的一個概念了,甚至很多人也會把閉包和匿名函數混淆。
閉包是有權訪問另一個函數作用域中的變量的函數。首先要明白的就是,閉包是函數。由於要求它可以訪問另一個函數的作用於中的變量,所以我們往往是在一個函數的內部創建另一個函數,而“另一個函數”就是閉包。
比如之前提到過的作為比較函數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function createComparisonFunction(propertyName){
return function (object1,object2){
var value1=object1[propertyName];
var value2=object2[propertyName];
if (value1<value2){
return -1;
} else if (value1>value2){
return 1;
} else {
return 0;
}
};
}
|
在這個函數中,由於return的函數它訪問了包含函數(外部函數)的變量propertyName,所以我們認為這個函數即為閉包。即使這個閉包被返回了,而且是在其他地方調用了,但是它仍然可以訪問propertyName,之所以還能夠訪問到propertyName這個變量,是因為內部函數(閉包)的作用域鏈中包含著createComparisonFunction函數的作用域。因此,要徹底搞清楚閉包,就需要徹底搞清楚函數被調用時發生了什麽以及作用域鏈的有關知識。
當某個函數被調用時,會創建一個執行環境(函數一旦被調用,則進入函數執行環境)和相應的作用域鏈(作用域鏈是隨著執行環境的不同而動態變化的)。(對於函數而言)之後使用arguments和其他命名參數的值來初始化函數的活動對象(每個執行環境都有一個變量對象,對於函數成為活動對象)。對於有閉包的函數而言,在作用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象始終處於第三位。。。直至作為作用域鏈終點的全局執行環境。
下面撇開閉包不談,先通過一個簡單的例子來理解作用域鏈以及變量對象和活動對象。
1 2 3 4 5 6 7 8 9 |
function compare(value1,value2){
if (value1<value2){
return -1;
} else if (value1>value2){
return 1;
} else {
return 0;
}
}<br> var result=compare(5,10);
|
以上代碼首先定義了compare()函數,然後又在全局作用域中調用了它。當調用compare函數時,首先創建一個函數執行環境,每個執行環境又對應這一個變量對象,也就是說作用域鏈和函數執行環境是同時創建的,其中作用域鏈的前端即為compare函數的活動對象(在函數中,變量對象又稱為活動對象)。在compare活動對象中包含了arguments、value1、value2(關鍵:盡管arguments數組對象包含value1和value2,但是我們還是要分開列舉,而不是僅僅認為只有arguments包含於compare的活動對象,因為value1和value2也包含於compare的活動對象)。
對於上述代碼而言,全局執行環境的變量對象(再次聲明:每一個執行環境都存在相應的變量對象)中包含result和compare,該變量對象在compare()執行環境的作用域鏈的第二位。
當我們創建compare()函數時,會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在compare函數內部的[[scope]]屬性中,當調用compare函數時,會為函數創建一個執行環境,然後通過復制函數的[[scope]]屬性中的對象構建起執行環境的作用域鏈。如下:
作用域鏈的本質就是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。無論什麽時候在函數中訪問一個變量,就會從作用域鏈的前端沿著作用域鏈搜索具有相應名字的變量。我們知道,全局環境的變量對象始終存在,而局部環境(如compare()函數執行環境)的變量對象只在函數執行的時候存在,一旦執行完畢,局部變量對象(活動對象)就會被銷毀。但在閉包中,卻與此不同。
把博文開始的代碼復制如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function createComparisonFunction(propertyName){
return function (object1,object2){
var value1=object1[propertyName];
var value2=object2[propertyName];
if (value1<value2){
return -1;
} else if (value1>value2){
return 1;
} else {
return 0;
}
};
}
|
由於在一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此,在createComparisonFunction函數內部定義的匿名函數的作用域中實際包含著外部函數的活動對象。如果我們執行如下代碼:
1 2 |
var compare=createComparisonFunction( "name" );
var result=compare({name: "zzw" },{name: "ht" });
|
這時候匿名函數的作用域鏈將引用著外部函數的活動對象。因為匿名函數從外部函數中被返回後,它的作用域鏈被初始化為包含外部函數的活動對象和全局變量對象。這樣,匿名函數就可以訪問外部函數中定義的所有變量。更為重要的是,即使外部函數在執行完畢後,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。換句話說,當createComparison()函數返回後,其執行環境的作用域鏈會被銷毀,但是她的活動對象仍然保存在內存中。等到倪敏函數被銷毀後,外部函數的活動對象才會被銷毀。
由於閉包會攜帶者包含他的函數的作用域,因此會比其他函數占用更多的內存。過度的使用閉包可能會導致內存占用過多,我們建議只在絕對必要的時候再考慮使用閉包。
模仿塊級作用域
1 2 3 4 5 6 |
( function (){
var now= new Date();
if (now.getMonth()==0&&now.getDate()==1){
alert( "happy new year" );
}
})();
|
這就是模仿塊級作用域,即定義並立即調用了一個匿名函數。
如下為演示其作用:
1 2 3 4 5 6 7 8 9 |
function outputNumbers(count){
( function (){
for ( var i=0;i<count;i++){
console.log(i);
}
})();
console.log(i);
}
outputNumbers(5);
|
這是在模仿塊級作用域之外的console.log(i)就會導致錯誤,因為i未被定義。說明在執行了模仿塊級作用域之後,內部的變量就被銷毀了。
第二部分:深入JavaScript閉包
閉包經典例子
Example 1
function numberGenerator() { var num = 1; function checkNumber() { console.log(num); } num++; return checkNumber; } var number = numberGenerator(); number(); // 2
這就是一個閉包,因為在numberGenerator中的函數checkNumber()保留了對num的引用,且在numberGenerator執行完了之後num不會由此被釋放,因為checkNumber還在保存著,所以不會消失。
他的好處就是我們可以很方便的取到在局部作用域的值, 當然如果你說我取到他的值只要設置成全局變量就行了啊,也不是不可以,但是最佳實踐是盡量減少局部變量的使用,如果僅僅是為了取到,還是使用閉包的好。
Example 2
function sayHello() { var say = function () { console.log(hello); }; var hello = "hello world"; return say; } var sayHelloClosure = sayHello(); sayHelloClosure();
在這個例子中我們可以看到閉包可以訪問得到外部封閉函數的所有變量,因為 hello 是在匿名函數之後定義的,但是仍然可以被訪問得到。
通過上面兩個例子,我們可以歸納為一句話:
定義在封閉函數中的變量,即使在該封閉函數已經執行完了之後,仍然能被訪問到。
執行上下文如下所示:
執行上下文是用來記錄代碼運行的環境的, 可以使全局上下文,也可以是進入某個函數內的上下文(註意:全局上下文不包括內部函數內部的上下文)
值得註意的是,程序自始至終只能進入一個執行上下文,這也就是為什麽說JavaScript是單線程的原因,即每次只能有一個命令在執行。
通常瀏覽器使用 棧 來維護執行上下文。 棧遵守後入先出的原則, 所以對應的執行上下文也是這樣。另外,程序並不需要執行完當前的執行上下文中的所有代碼再進入另一個,經常是當前執行上下文A執行到了一半就暫停,又進入了另一個B,當他在B中完全執行完了之後,又會回到A中,從它暫停的地方繼續執行。每次一個上下文被另一個上下文所替代時, 這個新的上下文就入棧成為棧頂,即當前的上下文。
說明: 為什麽很多函數喜歡用 for、bar、foobar來命名呢?
下面的一個例子,讓我們感受一下js執行過程中的棧的概念:
var a = 20; var b = 50; (function () { c = 30; c = a + 50; console.log(this); function goog () { var d = 20; d = d + 50; function bar() { var b = d + 15; console.log(b); } console.log(this); console.log(d); return bar; } var barCloure = goog(); barCloure(); console.log("444"); })(); var m = 50; console.log(m);
他在chrome中的斷點如下所示:
打了這麽多,是因為我希望進入一個執行環境就停下,我們可以仔細觀察每一個細節:
1.進入第一個語句執行時如下:
可以看到 Call Stack 和 Scope ,其中Call Stack 最直白的理解就是“叫它進棧”,且Call Stack中的第一個表示的就是當前的執行上下文,而後面的9表示現在運行的行數,程序執行到第10行就會為10.
註: 在當前執行環境下會執行直到12行,瀏覽器 認為第11行也是全局的, 12行就會進入一個匿名函數。
2. 程序執行到12行
可以看到執行到12行時, 我們顯然又進入了一個執行上下文,所以 Call Stack 讓這個新的執行上下文入棧,且當前在這個新的執行上下文執行,第13行, 而在棧底就是那個全局的執行上下文。
值得註意的是:在從全局的執行上下文進入到匿名函數的執行上下文中時,他會記住在執行完這個匿名函數後(匿名函數退棧後)應該接著從哪裏執行, 即第30行。 另外,瀏覽器認為在 <script>標簽下的js即我們認為的全局環境也是一個匿名函數的執行上下文。
另外,可以看到這時的Scope,即作用域是在一個Local作用域中,而他還引用者全局作用域,代表著他可以訪問全局作用域的所有變量和Local作用域中的所有變量和方法。 或更為概括的說:
在當前的執行上下文情況下, 我們可以訪問的到 Local 和 Global 下的所有屬性和方法。
且在Local中也指明了this是指向window的,這對於我們判斷有很大的好處。後面會講到這個例子。
3. 程序執行到26行後,由於調用了goog()函數,所以進入了第16行,進入了goog()個執行環境。
可以看到,我們已經進入了goog的執行環境,所以Call Stack已經讓goog執行上下文入棧,同時因為goog所在的匿名函數的執行上下文還沒有執行完,所以把它壓在了下面,並保留了暫停的位置,以便於goog退棧後記住重新開始的位置。
而在Scope中可以看到當前的作用域同樣是局部作用域(在js中作用域只有全局作用域和局部作用域的區別。)
直接看下面這張圖吧:
這裏由於bar在執行時對goog中的d引用了,所以在goog()執行完了之後d仍然沒有 被清除。
所以在作用域中顯示了 Local 對goog中的d的引用,而goog中的d還引用了外層的匿名函數的m, 所以這裏 bar 就是一個閉包。
Closures are functions that refer to independent (free) variables (variables that are used locally, but defined in an enclosing scope). In other words, these functions ‘remember‘ the environment in which they were created.
所以,bar記住的環境是 goog函數內的環境,而並沒有因為在goog執行結束之後就把環境丟棄了。
上述問題的代碼如下:
View Code
看官網講解會更好。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
即下面這種方式也是我們常用的:
function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); var add10 = makeAdder(10); console.log(add5(2)); // 7 console.log(add10(2)); // 12
大師牛人,寧有種乎?
JavaScript之閉包