區分初級和高級JavaScript程序員:是否理解JavaScript閉包(Closure)

閉包是functional language裡面的核心概念。也是常見的JS面試題,是否理解閉包是一個簡單的區分JS初級和高級程序員的判例。

區分初級和高級JavaScript程序員:是否理解JavaScript閉包(Closure)

幾乎每個JS程序員都在使用閉包,有意或無意間。比如編寫一個jQuery鼠標點擊處理函數:

$(function() {

var option;

$(".scssbox").click(function() { // 閉包,該閉包同時也是一個匿名函數

option = scss; // 閉包可以訪問和改變其外部函數(包含函數)中的變量

});

}

什麼是閉包

“閉包”實際上就是一個函數,該函數被定義在一個包容(外部)函數的內部,並能夠訪問外部函數的變量,即使外部函數已返回。

這種外部變量訪問能力不是通過數值拷貝傳遞,而是通過引用(Reference)傳遞的,因此閉包可以讀取並改變外部變量的取值。

function Name (firstName, lastName) {

var nameIntro = "Your name is ";

// 該內部函數能訪問外部函數中定義的變量,以及參數

function makeFullName () {

return nameIntro + firstName + " " + lastName;

}

return makeFullName ();

}

console.log( Name("Michael", "Jordan") ); // Your name is Michael Jordan

注意:上述代碼中的閉包不能被公開函數(public function)所調用,另外閉包不能訪問外部函數的arguments變量。所謂公開函數指的是通過原型(prototype)擴展來定義的對象函數。

為什麼需要閉包

閉包的好處是可以方便的完成外部函數對象的某些特定功能。我們可以類比C++等其他面向對象的語言,訪問控制是面嚮對象語言的一個基本特徵,而通過閉包,我們可以在JS語言中實現對象私有成員函數(private member function)和授權函數的功能。

閉包的陷阱

由於閉包能夠修改外部函數變量,如果不小心,可能會產生一些比較隱蔽的問題:

function Member(users) {

var i;

var uniqueID = 100;

for (i = 0; i < users.length; i++) {

users[i]["id"] = function() { return uniqueID + i;}

}

return users;

}

var users = [{name: "Ryan",id: 0},

{ name: "Mike",id: 0},

{name: "Mark",id: 0}];

var members = Member(users);

var rid = members[0];

console.log(rid.id()); // 103

上面的代碼本來是想給3個用戶分別分配唯一成員編號100、101、102,但實際上都是103。

原因就是在調用Member函數並返回後,i已經被修改為3,那麼當再次調用閉包函數獲取唯一編號時就已經是103。

實例分析

看下面代碼:

function f1(){

var n=999;

nAdd=function(){n+=1}

function f2(){alert(n);}

return f2;

}

var result=f1();

result(); // 999

nAdd();

result(); // 1000

在這段代碼中,result()它一共運行了兩次,第一次的值是999,第二次的值是1000。這證明了,函數f1中的局部變量n一直保存在內存中,並沒有在f1調用後被自動清除。為什麼會這樣呢?尤其是第二次,為何輸出的不是999呢? nAdd=function(){n+=1}又起到了什麼作用呢?

首先要說的是,閉包是functional language裡面的核心概念。當出現高階嵌套函數的時候,編譯器會做closure convention閉包變換,核心就是變量不在分配在stack上,而是分配在heap上。這就是為什麼f1已經返回,但是n還能被+1的原因。上面這段代碼,實際上就是一個高階嵌套函數。

  1. 因為在函數裡面有定義的函數,這是嵌套。pascal也是允許嵌套函數。

  2. 高階的原因是,函數可以所謂參數傳遞和返回,像我們熟悉的C語言。

但是當高階和嵌套同時出現,就會造成麻煩,所以pascal和C都只能支持其中的一個。

我來分析一下這個程序的執行流。

  1. var result=f1(); 返回了一個函數f2, 因此result為f2。這個高階函數特性,參考C語言函數指針。

  2. result(); 調用f2,顯然輸出999.

  3. nAdd(); 這裡需要注意,這個nAdd實際上在定義的時候是一個lambda,是一個匿名函數,功能是n+=1。定義時將這個函數賦值給nAdd。所以在此時,實際上是調用了n+=1.為什麼能找到n?因為n在堆裡面。

  4. result(); 調用f2,顯然輸出1000.

最後一點,n在堆上如何被銷燬,這個工作是垃圾收集器負責。當n不在被任何閉包的env引用的時候,會被回收。

相關推薦

推薦中...