JavaScript的“閉包”到底是什麼(2)
我的上篇部落格標題不對,造成一些誤解。我認為部落格的宗旨不是背教科書,而是分享研發心得。我的上篇標題因該改成“JavaScript 閉包的一個議題:它對outer scope 的影響”,因為我沒有嚴格地去分析閉包的定義,而是分析了實現閉包的其中一個語義問題。 講清楚閉包是件麻煩事,我也沒有看到什麼關於JavaScript的權威性著作(比如像C++語言有 Bjarne Stroustrup的C++ programming language)。所以除了苦讀JavaScript語言國際標準《Standard ECMA-262 specification》,我無法推薦一個論述“閉包”的最好的教材。 網友“穆己”的“scope chaining”的確是比較接近實質,但也不全面。我只好拋磚引玉,再做一次企圖。 閉包的含義包含了下列三個主要概念: Lexical Scope and Scope ChainLexical Scope的概念並不是Javacript發明,但是它作為JavaScript函數的組成部分,是一個在“傳統”函數概念上的附加值。 傳統函數(C, C++, Java, C#等)的lexical scope 和runtime scope 是一樣的。JavaScript 的lexical scope指的是函數定義時的“環境”,而不是函數運行時的環境。 對於一個特定函數來說,其”自由變數”是這個函數閉包中需要俘獲的主要內容。自由變數(本函數沒有定義的變數)的lexical capture(俘獲)順序是 (也就是scope chaining 的順序): A, 母函數的local 變數 B. 母函數的input argument C.在母函數的母函數中重複A,B,直到最頂層(GLOBAL scope) 在下面 的 myObj 的定義中: var x = 1000; // line 0 function myObj(x, y) { // Line1 this.func1 = function() { // Line2 x++; y --; } this.get1 = function () { return x; } this.get2 = function () { return y; } var x = 0; // Line 3 } myObj.prototype.AddTwo = function(z) { return this.get1() + this.get2() + z; } var m1 = new myObj(10, 20); // Line 4 var m2 = new myObj(30, 70); // Line 5 console.log('m1.x: ' + m1.get1()); // Line 6 console.log('m1.y: ' + m1.get2()); // Line 7 console.log('m2.x: ' + m2.get1()); // Line 8 console.log('m2.y: ' + m2.get2()); // Line 9 對於上面的例子,如果不是lexical scope, line 6 ~ line 9 列印的應該是10, 20, 30, 70。 但是因為lexical scope俘獲順序,x 是0(見line 3), 所以列印的是:0, 20, 0, 70。 注釋掉line3,根據俘獲順序,列印的就成了10, 20, 30, 70。 把myObj(x, y)改成myObj(z, y),列印的就成了1000, 20, 1000, 70。其中 1000是從global裡(Line 0)俘獲的。 Lexical 俘獲是在parsing stage進行的上面的俘獲順序必須在函數的parsing階段進行。函數的資料結構中在parsing後已經包含了所有“俘獲變數的reference”,運行階段不會改變了。這就是為什麼上面的line 3定義的可以優先於input 參數x的原因。若是執行時capture, line 3 是在函數的定義之後,該capture的因該說是input 參數x了。 C,C++等編譯語言是直接翻譯成native 函數的,所有的函數運行資訊都靠stack frame來動態擷取。唯一和閉包有所接近的概念是“全程變數(global variable)”. 這些global變數在編譯時間也都轉換成記憶體位址,運行時可以“就地解決”,無需一個獨立的閉包。這些函數不是object,不需動態產生,所以無需一個“靜態`”的閉包。 JavaScript之所以需要一個獨立的閉包,本人認為是因為所有的JavaScript都是object,可以“動態產生”,但是定義(第一道parsing)卻是靜態,這個“靜態”的部分需要閉包,動態部分和傳統函數一樣,靠runtime context 支撐。 這種“實現上的複雜性”,是為了閉包所帶來的,處理非同步事件時的方便付出的代價。 Lexical 俘獲是reference不是value這是我的上篇部落格想要強調的地方。如果上面的myObj執行時,如果俘獲的是x的值,那麼這三個函數func1,get1,get2就不會有任何聯絡了。 因為俘獲的是x的reference, 所以上面三個函數所看到的x是同一個變數。 這一點很重要,因為JavaScript中的local 變數並不是都是heap中的。起碼 GOOGLE V2 就不是。但是上面line 3 的x必須在heap中“出生和生活”,否則func1,get1,get2就會在已經毀滅了的stack 變數x上工作,使得上面的程式變得毫無意義了。 也就是說,閉包的runtime代價是將所被閉包的變數從stack中轉到heap中。