jQuery片段:
1 var
2 // Will speed up references to window, and allows munging its name.
3 window = this,
4 // Will speed up references to undefined, and allows munging its name.
5 undefined,
6 // Map over jQuery in case of overwrite
7 _jQuery = window.jQuery,
8 // Map over the $ in case of overwrite
9 _$ = window.$,
10
11 jQuery = window.jQuery = window.$ = function( selector, context ) {
12 // The jQuery object is actually just the init constructor 'enhanced'
13 return new jQuery.fn.init( selector, context );
14 },
15
16 // A simple way to check for HTML strings or ID strings
17 // (both of which we optimize for)
18 quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,
19 // Is it a simple selector
20 isSimple = /^.[^:#\[\.,]*$/;
從這一節開始,我們剝掉jQuery的外衣,看看里面藏著些什么。前一節中曾經提及,如果單看外層的匿名函數,不看里面的實現的話,這個實現肯定不是閉包。但是,如果把jQuery的實現加上的話,這個肯定就是一種閉包應用。萬丈高樓平地起,要了解閉包應用,就首先要了解它的基礎。而這一節,我們遇到的片段,就是這個基礎的所在——變量。(雖然這個片段包含眾多知識點,但請容許我一個個慢慢分說。)
聲明變量
變量的英文名為variable,其前三個字母正是我們在JS聲明變量的關鍵字——var。那么,我們先來看一下如何去聲明一個變量:
1 /*
2 * 聲明變量的格式為
3 * var 變量名 初始化變量表達式列表(可選)
4 */
5 var a=1, b, c="test", d=a+b;// 雖然b還沒有初始化,但是聲明是合法的
6 alert(a);// "1"
7 alert(b);// "undefined"
8 alert(c);// "test"
9 alert(d);// "NaN"
10 alert(e);// 這里將引發編譯錯誤:"e"未定義
11 // 同樣地,如果在初始化中使用未定義的變量,也會引發編譯錯誤。
如上例所示,聲明變量需要使用var關鍵字,然后在空格后緊跟變量的名字。在聲明變量的同時,我們也可以選擇幫變量初始化。初始化的值可以是任何類型的值或表達式,但是,如果你嘗試使用未定義的變量名來初始化,JS的編譯器將會判定發生編譯錯誤,并阻止程序繼續往下運行。無論你是否對聲明的變量進行初始化,你都可以繼續聲明第二個變量而無須使用var關鍵字。你所需要的只是運算符“,”。(關于運算符將在稍后章節詳細討論。)但當你沒有對聲明的變量進行初始化時,變量將會被賦予值“undefined”——undefined也是JS的固有類型之一。PS:JS中使用的運算符必須是半角的英文字符,甚至空格也一樣。
重復聲明的變量?!
當我們聲明了一個變量,而又在后續的代碼中再次對他進行聲明,結果會怎么樣呢?或許在很多其他語言中,這都會引起重復定義的錯誤,但在JS中,這完全是合法的。并且,由于JS是弱數據類型,所以變量能被賦予任何類型的值。請看以下例子:
1 var a=1;
2 alert(typeof a); // "number"
3 var a;
4 alert(typeof a); // "number"
5 var a="1";
6 alert(typeof a); // "string"
7 a=new String("1");
8 alert(typeof a); // "object"
看完上面的例子,你可能會產生兩個疑問:
a)為什么第二個a還是number?
b)為什么第四個a是object?
為了解答第一個問題,我們首先要了解聲明一個變量到底是怎么運作的。而第二個問題,我們將他放到下一節再討論。
var 變量聲明的工作步驟
當我們使用var關鍵字去聲明變量的時候,JS解釋器將會進行如下操作:
1)預編譯javascript代碼塊中所有非函數塊內的var關鍵字;
2)生成變量名標識并在其所在作用域分配空間;
3)按代碼順序運行至第一個var關鍵字所在行;
4)按變量聲明列表表達式次序計算初始化表達式的值;
5)每計算完一條初始化表達式,就將其計算結果賦予給對應的聲明變量;
6)繼續運行后續代碼至下一var關鍵字;
7)重復4-7步到代碼塊結束;
8)繼續編譯運行下一個代碼塊。
PS:JS將以一個代碼塊,也就是一個script標簽為單位去運行一段JS代碼。
正是因為var的工作方式,實際上程序執行時,解釋器是根本看不到var關鍵字的。他執行的只是初始化表達式的賦值語句而已——所以問題a的答案就是例子中的第三句實際上什么事也沒有做。所以,你一點也不用為代碼中會否出現重復定義的變量名而煩惱。你真正需要擔心的是,初始化語句所產生的變量的值的變化是否如你預期。除此之外,請不要嘗試使用保留字作為變量名。這幾乎在所有語言中都必須遵循的規范。
另外,在函數塊中聲明變量的工作步驟也是類似的,但不同的是,他們是在函數運行時才創建的。
沒有var的變量聲明?!
很多朋友都應該有這個經驗——“我們根本不需要使用var來聲明變量也能直接賦值啊!”。這是因為JS解釋器在遇到賦值表達式的時候,會先在作用域鏈中尋找這個變量是否已經聲明。如果這個變量沒有聲明,則隱式強制為其在全局(Global)作用域中聲明,并將表達式的值賦予給該變量。
但究竟為什么會這樣呢?其實一切都源自于變量的獲取規則和作用域鏈的化合作用外加賦值運算符的催化作用。
作用域鏈
每個運行時的上下文都有與其對應的一個作用域。而作用域鏈正是把這些作用域連接起來的橋梁。它的作用與程序尋找某一變量標識有關:
1)JS解釋器會按調用的順序把作用域加進作用域鏈(像棧般早進入的作用域會在作用域鏈的底部);
2)然后在程序尋找某一變量標識時進入作用域鏈中的第一個索引,并在其中尋找該變量標識;
3)如果沒有找到該標識,則前往下一個索引繼續尋找;
4)如果已經找到該標識,則將該標識及其值返回;
5)當搜索到最后一個索引仍未能找到該標識,則在最后的索引上創建該標識,并使其值為null,最后返回該標識與值。
PS:而上述的第5步發生的前提是該標識處于賦值運算符表達式左側。
因此,當你沒有使用var聲明變量而直接使用對該變量作初始化操作(簡單賦值)時,JS會自動為你創建該空值標識,并讓它可以順利執行賦值語句。
變量與作用域鏈
從上面的描述,我們可以很輕易的看到變量與作用域鏈的關系。因為只要程序需要尋找變量,就必須通過作用域鏈。而前面所談及的閉包問題正是由此而來的。回想一下我們前面的示例代碼:
1 var abc=function(y){
2 var x=y;// 這個是局部變量
3 return function(){
4 alert(x++);// 就是這里調用了閉包特性中的一級函數局部變量的x,并對它進行操作
5 alert(y--);// 引用的參數變量也是自由變量
6 }}(5);// 初始化
7 abc();// "5" "5"
8 abc();// "6" "4"
9 abc();// "7" "3"
10 alert(x);// 報錯!“x”未定義!