隨著(zhù) Ajax 應用的流行,JavaScript 語(yǔ)言得到了越來(lái)越多的關(guān)注。開(kāi)發(fā)人員對 JavaScript 的使用也日益深入。 JavaScript 已經(jīng)不再只是用來(lái)為頁(yè)面添加一些花哨的效果,它已經(jīng)成為構建 Ajax 應用的重要基石。 JavaScript 作為一種專(zhuān)門(mén)設計用來(lái)在瀏覽器中執行的動(dòng)態(tài)語(yǔ)言,它有許多重要的特性,并且不同于傳統的 Java 或 C++ 語(yǔ)言。熟悉這些特性可以幫助開(kāi)發(fā)者更好的開(kāi)發(fā) Ajax 應用。本文章介紹了 JavaScript 語(yǔ)言中十三個(gè)比較重要的特性,包括 prototype、執行上下文、作用域鏈和閉包等。 null 與 undefined JavaScript 中一共有 5 種基本類(lèi)型,分別是 String、Number、Boolean、Null 和 Undefined 。前 3 種都比較好理解,后面兩種就稍微復雜一點(diǎn)。 Null 類(lèi)型只有一個(gè)值,就是 null ; Undefined 類(lèi)型也只有一個(gè)值,即 undefined 。 null 和 undefined 都可以作為字面量(literal)在 JavaScript 代碼中直接使用。 null 與對象引用有關(guān)系,表示為空或不存在的對象引用。當聲明一個(gè)變量卻沒(méi)有給它賦值的時(shí)候,它的值就是 undefined 。 undefined 的值會(huì )出現在如下情況: - 從一個(gè)對象中獲取某個(gè)屬性,如果該對象及其 prototype 鏈 中的對象都沒(méi)有該屬性的時(shí)候,該屬性的值為 undefined 。
- 一個(gè) function 如果沒(méi)有顯式的通過(guò) return 來(lái)返回值給其調用者的話(huà),其返回值就是 undefined 。有一個(gè)特例就是在使用new的時(shí)候。
- JavaScript 中的 function 可以聲明任意個(gè)形式參數,當該 function 實(shí)際被調用的時(shí)候,傳入的參數的個(gè)數如果小于聲明的形式參數,那么多余的形式參數的值為 undefined 。
關(guān)于 null 和 undefined 有一些有趣的特性: - 如果對值為 null 的變量使用 typeof 操作符的話(huà),得到的結果是 object ;而對 undefined 的值使用 typeof,得到的結果是 undefined 。如
typeof null === "object";typeof undefined === "undefined" null == undefined,但是 null !== undefined
if ("" || 0) 對于 if 表達式,大家都不陌生。 JavaScript 中 if 后面緊跟的表達式的真假值判斷與其它語(yǔ)言有所不同。具體請看表 1。 表 1. JavaScript 中的真假值 | 類(lèi)型 | 真假值 | | Null | 總是為假(false) | | Undefined | 總是為假(false) | | Boolean | 保持真假值不變 | | Number | +0,-0 或是 NaN 的時(shí)候為假,其它值為真 | | String | 空字符串的時(shí)候為假,其它值為真 | | Object | 總是為真(true) | 從表 1中可以看到,在 JavaScript 中使得 if 判斷為假的值可能有 null、undefined、false、+0、-0、NaN 和空字符串("")。
== 與 === JavaScript 中有兩個(gè)判斷值是否相等的操作符,== 與 === 。兩者相比,== 會(huì )做一定的類(lèi)型轉換;而 === 不做類(lèi)型轉換,所接受的相等條件更加嚴格。 === 操作符的判斷算法 在使用 === 來(lái)判斷兩個(gè)值是否相等的時(shí)候,如判斷x===y,會(huì )首先比較兩個(gè)值的類(lèi)型是否相等,如果不相等的話(huà),直接返回 false 。接著(zhù)根據 x 的類(lèi)型有不同的判斷邏輯。 - 如果 x 的類(lèi)型是 Undefined 或 Null,則返回 true 。
- 如果 x 的類(lèi)型是 Number,只要 x 或 y 中有一個(gè)值為 NaN,就返回 false ;如果 x 和 y 的數字值相等,就返回 true ;如果 x 或 y 中有一個(gè)是 +0,另外一個(gè)是 -0,則返回 true 。
- 如果 x 的類(lèi)型是 String,當 x 和 y 的字符序列完全相同時(shí)返回 true,否則返回 false 。
- 如果 x 的類(lèi)型是 Boolean,當 x 和 y 同為 true 或 false 時(shí)返回 true,否則返回 false 。
- 當 x 和 y 引用相同的對象時(shí)返回 true,否則返回 false 。
== 操作符的判斷算法 在使用 == 來(lái)判斷兩個(gè)值是否相等的時(shí)候,如判斷x==y,當 x 和 y 的類(lèi)型一樣的時(shí)候,判斷邏輯與 === 一樣;如果 x 和 y 的類(lèi)型不一樣,== 不是簡(jiǎn)單的返回 false,而是會(huì )做一定的類(lèi)型轉換。 - 如果 x 和 y 中有一個(gè)是 null,另外一個(gè)是 undefined 的話(huà),返回 true 。如
null == undefined。 - 如果 x 和 y 中一個(gè)的類(lèi)型是 String,另外一個(gè)的類(lèi)型是 Number 的話(huà),會(huì )將 String 類(lèi)型的值轉換成 Number 來(lái)比較。如
3 == "3"。 - 如果 x 和 y 中一個(gè)的類(lèi)型是 Boolean 的話(huà),會(huì )將 Boolean 類(lèi)型的值轉換成 Number 來(lái)比較。如
true == 1、true == "1" - 如果 x 和 y 中一個(gè)的類(lèi)型是 String 或 Number,另外一個(gè)的類(lèi)型是 Object 的話(huà),會(huì )將 Object 類(lèi)型的值轉換成基本類(lèi)型來(lái)比較。如
[3,4] == "3,4" 需要注意的是 == 操作符不一定是傳遞的,即從A == B, B == C并不能一定得出A == C??紤]下面的例子,var str1 = new String("Hello"); var str2 = new String("Hello"); str1 == "Hello"; str2 == "Hello",但是str1 != str2。
Array JavaScript 中的數組(Array)和通常的編程語(yǔ)言,如 Java 或是 C/C++ 中的有很大不同。在 JavaScript 中的對象就是一個(gè)無(wú)序的關(guān)聯(lián)數組,而 Array 正是利用 JavaScript 中對象的這種特性來(lái)實(shí)現的。在 JavaScript 中,Array 其實(shí)就是一個(gè)對象,只不過(guò)它的屬性名是整數,另外有許多額外的屬性(如 length)和方法(如 splice)等方便地操作數組。 創(chuàng )建數組 創(chuàng )建一個(gè) Array 對象有兩種方式,一種是以數組字面量的方式,另外一種是使用 Array 構造器。數組字面量的方式通常為大家所熟知。如var array1 = [2, 3, 4];。使用 Array 構造器有兩種方式,一種是var array2 = new Array(1, 2, 3);;另外一種是var array3 = Array(1, 2, 3);。這兩種使用方式的是等價(jià)的。使用 Array 構造器的時(shí)候,除了以初始元素作為參數之后,也可以使用數組大小作為參數。如var array4 = new Array(3);用來(lái)創(chuàng )建一個(gè)初始大小為 3 的數組,其中每個(gè)元素都是 undefined 。 Array 的方法 JavaScript 中的 Array 提供了很多方法。 push和pop在數組的末尾進(jìn)行操作,使得數組可以作為一個(gè)棧來(lái)使用。 shift和unshift在數組的首部進(jìn)行操作。 slice(start, end)用來(lái)取得原始數組的子數組。其中參數start和end都可以是負數。如果是負數的話(huà),實(shí)際使用的值是參數的原始值加上數組的長(cháng)度。如var array = [2, 3, 4, 5]; array.slice(-2, -1);等價(jià)于array.slice(2, 3)。 splice是最復雜的一個(gè)方法,它可以同時(shí)刪除和添加元素。該方法的第一個(gè)參數表示要刪除的元素的起始位置,第二個(gè)參數表示要刪除的元素個(gè)數,其余的參數表示要添加的元素。如代碼var array = [2, 3, 4, 5]; array.splice(1, 2, 6, 7, 8);執行之后,array中的元素為[2, 6, 7, 8, 5]。該方法的返回被刪除的元素。 length JavaScript 中數組的 length 屬性與其他語(yǔ)言中有很大的不同。在 Java 或是 C/C++ 語(yǔ)言中,數組的 length 屬性都是用來(lái)表示數組中的元素個(gè)數。而 JavaScript 中,length 屬性的值只是 Array 對象中最大的整數類(lèi)型的屬性的值加上 1 。當通過(guò) [] 操作符來(lái)給 Array 對象增加元素的時(shí)候,如果 [] 中表達式的值可以轉換為正整數,并且其值大于或等于 Array 對象當前的 length 的值的話(huà),length 的值被設置成該值加上 1 。 length 屬性也可以顯式的設置。如果要設置的值比原來(lái)的 length 值小的話(huà),該 Array 對象中所有大于或等于新值的整數鍵值的屬性都會(huì )被刪除。如代碼清單 1中所示。 清單 1. Array 的 length 屬性 var array = []; array[0] = "a"; array[100] = "b"; array.length; // 值為 101 array["3"] = "c"; array.length = 4; // 值為 "b" 的第 101 個(gè)元素被刪除 |
arguments 在 JavaScript 中,在一個(gè) function 內部可以使用 arguments 對象。該對象中包含了 function 被調用時(shí)的實(shí)際參數的值。 arguments 對象雖然在功能上有些類(lèi)似數組(Array),但是它不是數組。 arguments 對象與數組的類(lèi)似體現在它有一個(gè) length 屬性,同時(shí)實(shí)際參數的值可以通過(guò) [] 操作符來(lái)獲取。但是 arguments 對象并沒(méi)有數組可以使用的 push、pop、splice 等 function 。其原因是 arguments 對象的 prototype 指向的是 Object.prototype 而不是 Array.prototype 。 使用 arguments 模擬重載 Java 和 C++ 語(yǔ)言都支持方法重載(overloading),即允許出現名稱(chēng)相同但是形式參數不同的方法;而 JavaScript 并不支持這種方式的重載。因為 JavaScript 中的 function 對象也是以屬性的形式出現的,在一個(gè)對象中增加與已有 function 同名的新 function 時(shí),舊的 function 對象會(huì )被覆蓋。不過(guò)可以通過(guò)使用 arguments 來(lái)模擬重載,其實(shí)現機制是通過(guò)判斷 arguments 中實(shí)際參數的個(gè)數和類(lèi)型來(lái)執行不同的邏輯。如代碼清單 2中所示。 清單 2. 使用 arguments 模擬重載示例 function sayHello() { switch (arguments.length) { case 0: return "Hello"; case 1: return "Hello, " + arguments[0]; case 2: return (arguments[1] == "cn" ? " 你好," : "Hello, ") + arguments[0]; }; } sayHello(); // 結果是 "Hello" sayHello("Alex"); // 結果是 "Hello, Alex" sayHello("Alex", "cn"); // 結果是 " 你好,Alex" | arguments.callee callee 是 arguments 對象的一個(gè)屬性,其值是當前正在執行的 function 對象。它的作用是使得匿名 function 可以被遞歸調用。下面以一段計算斐波那契序列(Fibonacci sequence)中第 N 個(gè)數的值的代碼來(lái)演示 arguments.callee 的使用,見(jiàn)代碼清單 3。 清單 3. arguments.callee 示例 function fibonacci(num) { return (function(num) { if (typeof num !== "number") return -1; num = parseInt(num); if (num < 1) return -1; if (num == 1 || num == 2) return 1; return arguments.callee(num - 1) + arguments.callee(num - 2); })(num); } fibonacci(100); |
prototype 與繼承 JavaScript 中的每個(gè)對象都有一個(gè) prototype 屬性,指向另外一個(gè)對象。使用對象字面量創(chuàng )建的對象的 prototype 指向的是Object.prototype,如var obj = {"name" : "Alex"};中創(chuàng )建的對象obj的 prototype 指向的就是Object.prototype。而使用 new 操作符創(chuàng )建的對象的 prototype 指向的是其構造器的 prototype 。如var users = new Array();中創(chuàng )建的對象users的 prototype 指向的是Array.prototype。由于一個(gè)對象 A 的 prototype 指向的是另外一個(gè)對象 B,而對象 B 自己的 prototype 又指向另外一個(gè)對象 C,這樣就形成了一個(gè)鏈條,稱(chēng)為 prototype 鏈。這個(gè)鏈條會(huì )不斷繼續,一直到Object.prototype。Object.prototype對象的 prototype 值為 null,從而使得該鏈條終止。圖 1中給出了 prototype 鏈的示意圖。 圖 1. JavaScript prototype 鏈示意圖 在圖 1中,studentA是通過(guò) new 操作符創(chuàng )建的,因此它的 prototype 指向其構造器Student的 prototype ;Student.prototype的值是通過(guò) new 操作符創(chuàng )建的,其 prototype 指向構造器Person的 prototype 。studentA的 prototype 鏈在圖 1中用虛線(xiàn)表示。 prototype 鏈在屬性查找過(guò)程中會(huì )起作用。當在一個(gè)對象中查找某個(gè)特定名稱(chēng)的屬性時(shí),會(huì )首先檢查該對象本身。如果找到的話(huà),就返回該屬性的值;如果找不到的話(huà),會(huì )檢查該對象的 prototype 指向的對象。如此下去直到找到該屬性,或是當前對象的 prototype 為 null 。 prototype 鏈在設置屬性的值時(shí)并不起作用。當設置一個(gè)對象中某個(gè)屬性的值的時(shí)候,如果當前對象中存在這個(gè)屬性,則更新其值;否則就在當前對象中創(chuàng )建該屬性。 JavaScript 中并沒(méi)有 Java 或 C++ 中類(lèi)(class)的概念,而是通過(guò) prototype 鏈來(lái)實(shí)現基于 prototype 的繼承。在 Java 中,狀態(tài)包含在對象實(shí)例中,方法包含在類(lèi)中,繼承只發(fā)生在結構和行為上。而在 JavaScript 中,狀態(tài)和方法都包含在對象中,結構、行為和狀態(tài)都是被繼承的。這里需要注意的是 JavaScript 中的狀態(tài)也是被繼承的,也就是說(shuō),在構造器的 prototype 中的屬性是被所有的實(shí)例共享的。如代碼清單 4中所示。 清單 4. JavaScript 中狀態(tài)被繼承的示例 function Student(name) { this.name = name; } Student.prototype.selectedCourses = []; Student.prototype.addCourse = function(course) { this.selectedCourses.push(course); } Student.prototype.outputCourses = function() { alert(this.name + " 選修的課程是:" + this.selectedCourses.join(",")); } var studentA = new Student("Alex"); var studentB = new Student("Bob"); studentA.addCourse(" 算法分析與設計 "); studentB.addCourse(" 數據庫原理 "); studentA.outputCourses(); // 輸出是“ Alex 選修的課程是算法分析與設計 , 數據庫原理” studentB.outputCourses(); // 輸出同上 | 代碼清單 4中的問(wèn)題在于將selectedCourses作為 prototype 的屬性之后,studentA和studentB兩個(gè)實(shí)例共享了該屬性,它們操作的實(shí)際是同樣的數據。
this JavaScript 中的 this 一直是容易讓人誤用的,尤其對于熟悉 Java 的程序員來(lái)說(shuō),因為 JavaScript 中的 this 與 Java 中的 this 有很大不同。在一個(gè) function 的執行過(guò)程中,如果變量的前面加上了 this 作為前綴的話(huà),如this.myVal,對此變量的求值就從 this 所表示的對象開(kāi)始。 this 的值取決于 function 被調用的方式,一共有四種,具體如下: - 如果一個(gè) function 是一個(gè)對象的屬性,該 funtion 被調用的時(shí)候,this 的值是這個(gè)對象。如果 function 調用的表達式包含句點(diǎn)(.)或是 [],this 的值是句點(diǎn)(.)或是 [] 之前的對象。如
myObj.func和myObj["func"]中,func被調用時(shí)的 this 是myObj。 - 如果一個(gè) function 不是作為一個(gè)對象的屬性,那么該 function 被調用的時(shí)候,this 的值是全局對象。當一個(gè) function 中包含內部 function 的時(shí)候,如果不理解 this 的正確含義,很容易造成錯誤。這是由于內部 function 的 this 值與它外部的 function 的 this 值是不一樣的。代碼清單 5中,在
myObj的func中有個(gè)內部名為inner的 function,在inner被調用的時(shí)候,this 的值是全局對象,因此找不到名為myVal的變量。這個(gè)時(shí)候通常的解決辦法是將外部 function 的 this 值保存在一個(gè)變量中(此處為self),在內部 function 中使用它來(lái)查找變量。 - 如果在一個(gè) function 之前使用 new 的話(huà),會(huì )創(chuàng )建一個(gè)新的對象,該 funtion 也會(huì )被調用,而 this 的值是新創(chuàng )建的那個(gè)對象。如
function User(name) {this.name = name}; var user1 = new User("Alex");中,通過(guò)調用new User("Alex"),會(huì )創(chuàng )建一個(gè)新的對象,以user1來(lái)引用,User這個(gè) function 也會(huì )被調用,會(huì )在user1這個(gè)對象中設置名為name的屬性,其值是Alex。 - 可以通過(guò) function 的 apply 和 call 方法來(lái)指定它被調用的時(shí)候的 this 的值。 apply 和 call 的第一個(gè)參數都是要指定的 this 的值,兩者不同的是調用的實(shí)際參數在 apply 中是以數組的形式作為第二個(gè)參數傳入的,而 call 中除了第一個(gè)參數之外的其它參數都是調用的實(shí)際參數。如
func.apply(anotherObj, [arg1, arg2])中,func調用時(shí)候的 this 指的是anotherObj,兩個(gè)參數分別是arg1和arg2。同樣的功能用 call 來(lái)寫(xiě)則是func.call(anotherObj, arg1, arg2)。 清單 5. 內部 function 的 this 值 var myObj = { myVal : "Hello World", func : function() { alert(typeof this.myVal); // 結果為 string var self = this; function inner() { alert(typeof this.myVal); // 結果為 undefined alert(typeof self.myVal); // 結果為 string } inner(); } }; myObj.func(); |
new JavaScript 中并沒(méi)有 Java 或是 C++ 中的類(lèi)(class)的概念,而是采用構造器(constructor)的方式來(lái)創(chuàng )建對象。在 new 表達式中使用構造器就可以創(chuàng )建新的對象。由構造器創(chuàng )建出來(lái)的對象有一個(gè)隱含的引用指向該構造器的 prototype 。 所有的構造器都是對象,但并不是所有的對象都能成為構造器。能作為構造器的對象必須實(shí)現隱含的[[Construct]方法。如果 new 操作符后面的對象并不是構造器的話(huà),會(huì )拋出 TypeError 異常。 new 操作符會(huì )影響 function 調用中 return 語(yǔ)句的行為。當 function 調用的時(shí)候有 new 作為前綴,如果 return 的結果不是一個(gè)對象,那么新創(chuàng )建的對象將會(huì )被返回。在代碼清單 6中,functionanotherUser中通過(guò) return 語(yǔ)句返回了一個(gè)對象,因此u2引用的是返回的那個(gè)對象;而 functionuser并沒(méi)有使用 return 語(yǔ)句,因此u1引用的是新創(chuàng )建的user對象。 清單 6. new 操作符對 return 語(yǔ)句行為的影響 function user(name) { this.name = name; } function anotherUser(name) { this.name = name; return {"badName" : name}; } var u1 = new user("Alex"); alert(typeof u1.name); // 結果為 string var u2 = new anotherUser("Alex"); alert(typeof u2.name); // 結果為 undefined alert(typeof u2.badName); // 結果為 string |
eval JavaScript 中的 eval 可以用來(lái)解釋執行一段 JavaScript 程序。當傳給 eval 的參數的值是字符串的時(shí)候,該字符串會(huì )被當成一段 JavaScript 程序來(lái)執行。 隱式的 eval 除了顯式的調用 eval 之外,JavaScript 中的有些 function 能接受字符形式的 JavaScript 代碼并執行,這相當于隱式的調用了 eval 。這些 function 的典型代表是setTimeout和setInterval。具體請見(jiàn)代碼清單 7。由于 eval 的性能比較差,所以在使用setTimeout和setInterval等 function 的時(shí)候,最好傳入 function 的引用,而不是字符串。 清單 7. 隱式的 eval 示例 var obj = { show1 : function() { alert(" 時(shí)間到! "); }, show2 : function() { alert("10 秒一次的提醒! "); }; }; setTimeout(obj.show1, 1000); setTimeout("obj.show1();", 2000); setInterval(obj.show2, 10000); setInterval("obj.show2();", 10000); | eval 的潛在危險 在目前的 Ajax 應用中,JSON 是一種流行的瀏覽器端和服務(wù)器端處之間傳輸數據的格式。服務(wù)器端傳過(guò)來(lái)的數據在瀏覽器端通過(guò) JavaScript 的 eval 方法轉換成可以直接使用的對象。然而,在瀏覽器端執行任意的 JavaScript 會(huì )帶來(lái)潛在的安全風(fēng)險,惡意的 JavaScript 代碼可能會(huì )破壞應用。對于這個(gè)問(wèn)題,有兩種解決方式: - 帶注釋的 JSON(JSON comments filtering)和帶前綴的 JSON(JSON prefixing)
- 這兩種方法都是 Dojo 中用來(lái)避免 JSON 劫持(JSON hijacking)的。帶注釋的 JSON 指的是從服務(wù)器端返回的 JSON 數據都是帶有注釋的,瀏覽器端的 JavaScript 代碼需要先去掉注釋的標記,再通過(guò) eval 來(lái)獲得 JSON 數據。這種方法一度被廣泛使用,后來(lái)被證明并不安全,還會(huì )引入其它的安全漏洞。帶前綴的 JSON 是目前推薦使用的方法,這種方法的使用非常簡(jiǎn)單,只需要在從服務(wù)器端的 JSON 字符串之前加上
{} &&,再調用 eval 。關(guān)于這兩種方法的細節,請看參考資料。 - 對 JSON 字符串進(jìn)行語(yǔ)法檢查
- 安全的 JSON 應該是不包含賦值和方法調用的。在 JSON 的 RFC 4627 中,給出了判斷 JSON 字符串是否安全的方法,是通過(guò)兩個(gè)正則表達式來(lái)實(shí)現的。具體見(jiàn)代碼清單 8。關(guān)于 RFC 4627 的細節,請看參考資料。
清單 8. RFC 4627 中給出的檢查 JSON 字符串的方法 var my_JSON_object = !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test( text.replace(/"(\\.|[^"\\])*"/g, ''))) && eval('(' + text + ')'); |
執行上下文(execution context)和作用域鏈(scope chain) 執行上下文(execution context)是 ECMAScript 規范(請看參考資料)中用來(lái)描述 JavaScript 代碼執行的抽象概念。所有的 JavaScript 代碼都是在某個(gè)執行上下文中運行的。在當前執行上下文中調用 function 的時(shí)候,會(huì )進(jìn)入一個(gè)新的執行上下文。當該 function 調用結束的時(shí)候,會(huì )返回到原來(lái)的執行上下文中。如果 function 調用過(guò)程中拋出異常,并沒(méi)有被捕獲的話(huà),有可能從多個(gè)執行上下文中退出。在 function 調用過(guò)程,也可能調用其它的 function,從而進(jìn)入新的執行上下文。由此形成一個(gè)執行上下文棧。 每個(gè)執行上下文都與一個(gè)作用域鏈(scope chain)關(guān)聯(lián)起來(lái)。該作用域鏈用來(lái)在 function 執行時(shí)求標識符(Identifier)的值。在該鏈中包含多個(gè)對象。在對標識符進(jìn)行求值的過(guò)程中,會(huì )從鏈首的對象開(kāi)始,然后依次查找后面的對象,直到在某個(gè)對象中找到與標識符名稱(chēng)相同的屬性。如”protype 鏈與繼承“中所述,在每個(gè)對象中進(jìn)行屬性查找的時(shí)候,會(huì )使用該對象的 prototype 鏈。在一個(gè)執行上下文中,與其關(guān)聯(lián)的作用域鏈只會(huì )被with語(yǔ)句和 catch 子句影響。 在進(jìn)入一個(gè)新的執行上下文的時(shí)候,會(huì )按順序執行下面的操作: - 創(chuàng )建激活(Activation)對象
激活對象是在進(jìn)入新的執行上下文的時(shí)候被創(chuàng )建出來(lái)的,并與新的執行上下文關(guān)聯(lián)起來(lái)。在初始化的時(shí)候,該對象包含一個(gè)名為arguments的屬性。激活對象在變量初始化的時(shí)候也會(huì )被使用。 JavaScript 代碼不能直接訪(fǎng)問(wèn)該對象,但是可以訪(fǎng)問(wèn)該對象里面的成員(如 arguments)。 - 創(chuàng )建作用域鏈
接下來(lái)的操作是創(chuàng )建作用域鏈。每個(gè) function 都有一個(gè)內部屬性[[scope]],它的值是一個(gè)包含多個(gè)對象的鏈。該屬性的具體值與 function 的創(chuàng )建方式和在代碼中的位置有很大關(guān)系(見(jiàn)“function 對象的創(chuàng )建方式”)。這個(gè)步驟中的主要操作是將上一步中創(chuàng )建的激活對象添加到 function 的[[scope]]屬性對應的鏈的前面。 - 變量初始化
該步驟對 function 中需要使用的變量進(jìn)行初始化。初始化時(shí)使用的對象是第一步中所創(chuàng )建的激活對象,不過(guò)被稱(chēng)之為變量(Variable)對象。會(huì )被初始化的變量包括 function 調用時(shí)的實(shí)際參數、內部 function 和局部變量。在這個(gè)步驟中,對于局部變量,只是在變量對象中創(chuàng )建了同名的屬性,但是屬性的值為 undefined,只有在 function 執行過(guò)程中才會(huì )被真正賦值。 全局 JavaScript 代碼是在全局執行上下文中運行的,該上下文的作用域鏈只包含一個(gè)全局對象。 圖 2中給出了 function 執行過(guò)程中的作用域鏈的示意圖,其中的虛線(xiàn)表示作用域鏈。 圖 2. 作用域鏈示意圖
function a() {}、var a = function() {} 與 var a = new Function() 在 JavaScript 中,function 對象的創(chuàng )建方式有三種:function 聲明、function 表達式和使用 Function 構造器。通過(guò)這三種方法創(chuàng )建出來(lái)的 function 對象的[[scope]]屬性的值會(huì )有所不同,從而影響 function 執行過(guò)程中的作用域鏈。下面具體討論這三種情況。 - function 聲明
- function 聲明的格式是
function funcName() {}。使用 function 聲明的 function 對象是在進(jìn)入執行上下文時(shí)的變量初始化過(guò)程中創(chuàng )建的。該對象的[[scope]]屬性的值是它被創(chuàng )建時(shí)的執行上下文對應的作用域鏈。 - function 表達式
- function 表達式的格式是
var funcName = function() {}。使用 function 表達式的 function 對象是在該表達式被執行的時(shí)候創(chuàng )建的。該對象的[[scope]]屬性的值與使用 function 聲明創(chuàng )建的對象一樣。 - Function 構造器
- 對于 Function 構造器,大家可能比較陌生。聲明一個(gè) function 時(shí),通常使用前兩種方式。該方式的格式是
var funcName = new Function(p1, p2,..., pn, body),其中 p1、p2 到 pn 表示的是該 function 的形式參數,body 是 function 的內容。使用該方式的 function 對象是在構造器被調用的時(shí)候創(chuàng )建的。該對象的[[scope]]屬性的值總是一個(gè)只包含全局對象的作用域鏈。 function 對象的 length 屬性可以用來(lái)獲取聲明 function 時(shí)候指定的形式參數的個(gè)數。如前所述,function 對象被調用時(shí)的實(shí)際參數是通過(guò) arguments 來(lái)獲取的。
with with 語(yǔ)句的語(yǔ)法是with ( Expression ) Statement。 with 會(huì )把由 Expression 計算出來(lái)的對象添加到當前執行上下文的作用域鏈的前面,然后使用這個(gè)擴大的作用域鏈來(lái)執行語(yǔ)句 Statement,最后恢復作用域鏈。不管里面的語(yǔ)句是否正常退出,作用域鏈都會(huì )被恢復。 由于 with 語(yǔ)言會(huì )把額外的對象添加到作用域鏈的前面,使用 with 可能會(huì )影響性能并造成難以發(fā)現的錯誤。由于額外的對象在作用域鏈的前面,當執行到 with 里面的語(yǔ)句,需要對一個(gè)標識符求值的時(shí)候,會(huì )首先沿著(zhù)該對象的 prototype 鏈查找。如果找不到,才會(huì )依次查找作用域鏈中原來(lái)的對象。因此,如果在 with 里面的語(yǔ)句中頻繁引用不在額外對象的 prototype 鏈中的變量的話(huà),查找的速度會(huì )比不使用 with 慢。具體見(jiàn)代碼清單 9。 清單 9. with 的用法示例 function A() { this.a = "A"; } function B() { this.b = "B"; } B.prototype = new A(); function C() { this.c = "C"; } C.prototype = new B(); (function () { var myVar = "Hello World"; alert(typeof a); // 結果是 "undefined" var a = 1; var obj = new C(); with (obj) { alert(typeof a); // 結果是 "string" alert(myVar); // 查找速度比較慢 } alert(typeof a); // 結果是 "number" })(); | 在代碼中,首先通過(guò) prototype 的方式實(shí)現了繼承。在 with 中,執行alert(typeof a)需要查找變量 a,由于 obj 在作用域鏈的前面,而 obj 中也存在名為 a 的屬性,因此 obj 中的 a 被找到。執行alert(myVar)需要查找變量 myVal,而 obj 中不存在名為 myVal 的屬性,會(huì )繼續查找作用域鏈中后面的對象,因此比不使用 with 的速度慢。需要注意的是最后一條語(yǔ)句alert(typeof a),它不在 with 里面,因此查找到的 a 是之前聲明的 number 型的變量。
閉包 閉包(closure)是 JavaScript 中一個(gè)非常強大的功能。如果使用得當的話(huà),可以使得代碼更簡(jiǎn)潔,并實(shí)現在其它語(yǔ)言中很難實(shí)現的功能;而如果使用不當的話(huà),則會(huì )導致難以調試的錯誤,也可能造成內存泄露。只有在充分理解閉包的基礎上,才能正確的使用它。理解閉包需要首先理解 JavaScript 中的prototype 鏈、執行上下文和作用域鏈等概念。 閉包指的是一個(gè)表達式(通常是一個(gè) function),該表達式可以有自由的變量,并且運行環(huán)境能夠正確的獲取這些變量的值。 JavaScript 中閉包的產(chǎn)生是由于 JavaScript 中允許內部 function,也就是在一個(gè) function 內部聲明的 function 。內部 function 可以訪(fǎng)問(wèn)外部 function 中的局部變量、傳入的參數和其它內部 function 。當內部 function 可以在包含它的外部 function 之外被引用時(shí),就形成了一個(gè)閉包。這個(gè)時(shí)候,即便外部 function 已經(jīng)執行完成,該內部 function 仍然可以被執行,并且其中所用到的外部 function 的局部變量、傳入的參數等仍然保留外部 function 執行結束時(shí)的值。 下面通過(guò)一個(gè)例子來(lái)說(shuō)明閉包的形成,見(jiàn)代碼清單 10。 清單 10. JavaScript 閉包示例代碼 function addBy(first) { function add(second) { return first + second; } return add; } var func = addBy(10); func(20); // 結果為 30 var newFunc = addBy(30); newFunc(20); // 結果為 50 | 在代碼清單 10中,外部 functionaddBy的內部 functionadd的引用被返回給addBy的調用者,同時(shí)add在方法體中使用了addBy的參數first。這樣就形成了一個(gè)閉包。通過(guò)調用addBy(10)得到的 functionfunc,在其之后的執行過(guò)程中,都會(huì )保留創(chuàng )建的時(shí)候使用的first參數的值10。 下面分析代碼清單 10中執行的細節。首先addBy(10)被調用。由于addBy是在全局代碼中聲明的,因此被調用時(shí)候的執行上下文對應的作用域鏈只包含全局對象。在addBy的方法體中,聲明了一個(gè)內部 functionadd。add的[[scope]]屬性會(huì )在作用域鏈之前加上 functionaddBy的激活對象。該對象中包含了經(jīng)過(guò)初始化的參數first,其值為10。至此,functionfunc的[[scope]]屬性的值是包含兩個(gè)對象。當func被調用的時(shí)候,會(huì )進(jìn)入一個(gè)新的執行上下文,而此時(shí)的作用域鏈的前面加上了 functionadd調用時(shí)的激活對象。該對象中包含了經(jīng)過(guò)初始化的參數second,其值為20。在func的執行過(guò)程中,需要對兩個(gè)標識符first和second求值的時(shí)候,會(huì )使用之前提到的包含三個(gè)對象的作用域鏈。從而可以正確的求值。 在 JavaScript 中,正確的使用閉包可以簡(jiǎn)化代碼。下面舉幾個(gè)例子來(lái)說(shuō)明。 避免名稱(chēng)空間沖突 在多人協(xié)作開(kāi)發(fā)應用,或是使用第三方開(kāi)發(fā)的 JavaScript 庫的時(shí)候,一個(gè)通常會(huì )遇到的問(wèn)題是名稱(chēng)空間沖突。比如第三方的 JavaScript 庫在全局對象中聲明了一個(gè)屬性叫test,如果在自己的代碼中也會(huì )聲明同樣名稱(chēng)的屬性的話(huà),當兩者一同使用的時(shí)候,后加載的屬性值會(huì )替換之前的值,從而造成錯誤。 這個(gè)時(shí)候典型的做法是只在全局對象中保存一個(gè)對象,所有的功能都通過(guò)引用此對象來(lái)完成。完成功能所需要的內部狀態(tài)都封裝在一個(gè)閉包中。如代碼清單 11所示。 清單 11. 使用閉包避免名稱(chēng)空間沖突 (function() { if (typeof MyCode === "undefined") { var defaultName = "Alex"; MyCode = { "sayHello" : function(name) { alert("Hello, " + (name || defaultName)); } }; } })(); MyCode.sayHello(); // 輸出為 Hello, Alex MyCode.sayHello("Bob"); // 輸出為 Hello, Bob | 代碼中通過(guò)創(chuàng )建一個(gè)匿名 function 并立即執行來(lái)生成一個(gè)閉包。在閉包中,通過(guò)修改全局對象MyCode來(lái)添加所需的功能。內部狀態(tài)之一的屬性defaultName被封裝在閉包中,不能被閉包之外的代碼所引用,也不會(huì )引發(fā)命名沖突。 保存狀態(tài) 在 JavaScript 代碼運行過(guò)程中,不可避免的需要保存一些內部狀態(tài)。通過(guò)使用閉包,可以將內部狀態(tài)封裝在一個(gè) function 內部,使得代碼更加簡(jiǎn)潔。如代碼清單 12所示。 清單 12. 使用閉包保存狀態(tài) var getNextId = (function() { var id = 1; return function() { return id++; } })(); getNextId(); // 輸出 1 getNextId(); // 輸出 2 getNextId(); // 輸出 3 | 代碼中的getNextId的功能是生成惟一的 ID,因此它需要維護當前的 ID 這樣一個(gè)狀態(tài)。通過(guò)使用閉包,不需要在全局對象中添加一個(gè)新的屬性,該屬性由閉包來(lái)維護。閉包之外的代碼也不能訪(fǎng)問(wèn)或修改getNextId的內部狀態(tài)。 折疊調用參數 在 JavaScript 中,有些 function,如setTimeout和setInterval,只接受一個(gè) function 作為參數。在有些情況下,這些 function 的執行是需要額外的參數的。這個(gè)時(shí)候可以通過(guò)使用閉包,將原始 function 的參數進(jìn)行折疊,得到一個(gè)沒(méi)有參數的新 function 。如代碼清單 13所示。 清單 13. 使用閉包折疊調用參數 function doSomething(a, b, c) { alert(a + b + c); } function fold(a, b, c) { return function() { doSomething(a, b, c); } } var newFunc = fold("Hello", " ", "World"); setTimeout(newFunc, 1000); // 輸出為 Hello World | 代碼中的doSomething需要三個(gè)參數來(lái)完成其功能。如果直接將doSomething傳給setTimeout的話(huà),三個(gè)參數的值都是 undefined 。fold將三個(gè)參數的值保存在激活對象,并添加在作用域鏈中。這樣即便返回的 function 是沒(méi)有參數的,它仍然可以獲得這三個(gè)參數的值。 關(guān)于閉包的更多內容,請參見(jiàn)參考資料。 |