JavaScript 技巧與高級特性

摘自:http://www.ibm.com/developerworks/cn/web/wa-lo-dojoajax1/

隨着 Ajax 應用的流行,JavaScript 語言得到了越來越多的關注。開發人員對 JavaScript 的使用也日益深入。 JavaScript 已經不再只是用來爲頁面添加一些花哨的效果,它已經成爲構建 Ajax 應用的重要基石。 JavaScript 作爲一種專門設計用來在瀏覽器中執行的動態語言,它有許多重要的特性,並且不同於傳統的 Java 或 C++ 語言。熟悉這些特性可以幫助開發者更好的開發 Ajax 應用。本文章介紹了 JavaScript 語言中十三個比較重要的特性,包括 prototype、執行上下文、作用域鏈和閉包等。

null 與 undefined

JavaScript 中一共有 5 種基本類型,分別是 String、Number、Boolean、Null 和 Undefined 。前 3 種都比較好理解,後面兩種就稍微複雜一點。 Null 類型只有一個值,就是 null ; Undefined 類型也只有一個值,即 undefined 。 null 和 undefined 都可以作爲字面量(literal)在 JavaScript 代碼中直接使用。

null 與對象引用有關係,表示爲空或不存在的對象引用。當聲明一個變量卻沒有給它賦值的時候,它的值就是 undefined 。

undefined 的值會出現在如下情況:

  • 從一個對象中獲取某個屬性,如果該對象及其 prototype 鏈 中的對象都沒有該屬性的時候,該屬性的值爲 undefined 。
  • 一個 function 如果沒有顯式的通過 return 來返回值給其調用者的話,其返回值就是 undefined 。有一個特例就是在使用new的時候。
  • JavaScript 中的 function 可以聲明任意個形式參數,當該 function 實際被調用的時候,傳入的參數的個數如果小於聲明的形式參數,那麼多餘的形式參數的值爲 undefined 。

關於 null 和 undefined 有一些有趣的特性:

  • 如果對值爲 null 的變量使用 typeof 操作符的話,得到的結果是 object ;而對 undefined 的值使用 typeof,得到的結果是 undefined 。如 typeof null === "object";typeof undefined === "undefined"
  • null == undefined,但是 null !== undefined





回頁首


if ("" || 0)

對於 if 表達式,大家都不陌生。 JavaScript 中 if 後面緊跟的表達式的真假值判斷與其它語言有所不同。具體請看表 1


表 1. JavaScript 中的真假值
類型 真假值
Null 總是爲假(false)
Undefined 總是爲假(false)
Boolean 保持真假值不變
Number +0,-0 或是 NaN 的時候爲假,其它值爲真
String 空字符串的時候爲假,其它值爲真
Object 總是爲真(true)

表 1中可以看到,在 JavaScript 中使得 if 判斷爲假的值可能有 null、undefined、false、+0、-0、NaN 和空字符串("")。





回頁首


== 與 ===

JavaScript 中有兩個判斷值是否相等的操作符,== 與 === 。兩者相比,== 會做一定的類型轉換;而 === 不做類型轉換,所接受的相等條件更加嚴格。

=== 操作符的判斷算法

在使用 === 來判斷兩個值是否相等的時候,如判斷x===y,會首先比較兩個值的類型是否相等,如果不相等的話,直接返回 false 。接着根據 x 的類型有不同的判斷邏輯。

  • 如果 x 的類型是 Undefined 或 Null,則返回 true 。
  • 如果 x 的類型是 Number,只要 x 或 y 中有一個值爲 NaN,就返回 false ;如果 x 和 y 的數字值相等,就返回 true ;如果 x 或 y 中有一個是 +0,另外一個是 -0,則返回 true 。
  • 如果 x 的類型是 String,當 x 和 y 的字符序列完全相同時返回 true,否則返回 false 。
  • 如果 x 的類型是 Boolean,當 x 和 y 同爲 true 或 false 時返回 true,否則返回 false 。
  • 當 x 和 y 引用相同的對象時返回 true,否則返回 false 。

== 操作符的判斷算法

在使用 == 來判斷兩個值是否相等的時候,如判斷x==y,當 x 和 y 的類型一樣的時候,判斷邏輯與 === 一樣;如果 x 和 y 的類型不一樣,== 不是簡單的返回 false,而是會做一定的類型轉換。

  • 如果 x 和 y 中有一個是 null,另外一個是 undefined 的話,返回 true 。如null == undefined
  • 如果 x 和 y 中一個的類型是 String,另外一個的類型是 Number 的話,會將 String 類型的值轉換成 Number 來比較。如3 == "3"
  • 如果 x 和 y 中一個的類型是 Boolean 的話,會將 Boolean 類型的值轉換成 Number 來比較。如true == 1true == "1"
  • 如果 x 和 y 中一個的類型是 String 或 Number,另外一個的類型是 Object 的話,會將 Object 類型的值轉換成基本類型來比較。如[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)和通常的編程語言,如 Java 或是 C/C++ 中的有很大不同。在 JavaScript 中的對象就是一個無序的關聯數組,而 Array 正是利用 JavaScript 中對象的這種特性來實現的。在 JavaScript 中,Array 其實就是一個對象,只不過它的屬性名是整數,另外有許多額外的屬性(如 length)和方法(如 splice)等方便地操作數組。

創建數組

創建一個 Array 對象有兩種方式,一種是以數組字面量的方式,另外一種是使用 Array 構造器。數組字面量的方式通常爲大家所熟知。如var array1 = [2, 3, 4];。使用 Array 構造器有兩種方式,一種是var array2 = new Array(1, 2, 3);;另外一種是var array3 = Array(1, 2, 3);。這兩種使用方式的是等價的。使用 Array 構造器的時候,除了以初始元素作爲參數之後,也可以使用數組大小作爲參數。如var array4 = new Array(3);用來創建一個初始大小爲 3 的數組,其中每個元素都是 undefined 。

Array 的方法

JavaScript 中的 Array 提供了很多方法。

  • pushpop在數組的末尾進行操作,使得數組可以作爲一個棧來使用。
  • shiftunshift在數組的首部進行操作。
  • slice(start, end)用來取得原始數組的子數組。其中參數startend都可以是負數。如果是負數的話,實際使用的值是參數的原始值加上數組的長度。如var array = [2, 3, 4, 5]; array.slice(-2, -1);等價於array.slice(2, 3)
  • splice是最複雜的一個方法,它可以同時刪除和添加元素。該方法的第一個參數表示要刪除的元素的起始位置,第二個參數表示要刪除的元素個數,其餘的參數表示要添加的元素。如代碼var array = [2, 3, 4, 5]; array.splice(1, 2, 6, 7, 8);執行之後,array中的元素爲[2, 6, 7, 8, 5]。該方法的返回被刪除的元素。

length

JavaScript 中數組的 length 屬性與其他語言中有很大的不同。在 Java 或是 C/C++ 語言中,數組的 length 屬性都是用來表示數組中的元素個數。而 JavaScript 中,length 屬性的值只是 Array 對象中最大的整數類型的屬性的值加上 1 。當通過 [] 操作符來給 Array 對象增加元素的時候,如果 [] 中表達式的值可以轉換爲正整數,並且其值大於或等於 Array 對象當前的 length 的值的話,length 的值被設置成該值加上 1 。 length 屬性也可以顯式的設置。如果要設置的值比原來的 length 值小的話,該 Array 對象中所有大於或等於新值的整數鍵值的屬性都會被刪除。如代碼清單 1中所示。


清單 1. Array 的 length 屬性
var array = []; 
 array[0] = "a"; 
 array[100] = "b"; 
 array.length;     // 值爲 101 
 array["3"] = "c"; 
 array.length = 4; // 值爲 "b" 的第 101 個元素被刪除





回頁首


arguments

在 JavaScript 中,在一個 function 內部可以使用 arguments 對象。該對象中包含了 function 被調用時的實際參數的值。 arguments 對象雖然在功能上有些類似數組(Array),但是它不是數組。 arguments 對象與數組的類似體現在它有一個 length 屬性,同時實際參數的值可以通過 [] 操作符來獲取。但是 arguments 對象並沒有數組可以使用的 push、pop、splice 等 function 。其原因是 arguments 對象的 prototype 指向的是 Object.prototype 而不是 Array.prototype 。

使用 arguments 模擬重載

Java 和 C++ 語言都支持方法重載(overloading),即允許出現名稱相同但是形式參數不同的方法;而 JavaScript 並不支持這種方式的重載。因爲 JavaScript 中的 function 對象也是以屬性的形式出現的,在一個對象中增加與已有 function 同名的新 function 時,舊的 function 對象會被覆蓋。不過可以通過使用 arguments 來模擬重載,其實現機制是通過判斷 arguments 中實際參數的個數和類型來執行不同的邏輯。如代碼清單 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 對象的一個屬性,其值是當前正在執行的 function 對象。它的作用是使得匿名 function 可以被遞歸調用。下面以一段計算斐波那契序列(Fibonacci sequence)中第 N 個數的值的代碼來演示 arguments.callee 的使用,見代碼清單 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 中的每個對象都有一個 prototype 屬性,指向另外一個對象。使用對象字面量創建的對象的 prototype 指向的是Object.prototype,如var obj = {"name" : "Alex"};中創建的對象obj的 prototype 指向的就是Object.prototype。而使用 new 操作符創建的對象的 prototype 指向的是其構造器的 prototype 。如var users = new Array();中創建的對象users的 prototype 指向的是Array.prototype。由於一個對象 A 的 prototype 指向的是另外一個對象 B,而對象 B 自己的 prototype 又指向另外一個對象 C,這樣就形成了一個鏈條,稱爲 prototype 鏈。這個鏈條會不斷繼續,一直到Object.prototypeObject.prototype對象的 prototype 值爲 null,從而使得該鏈條終止。圖 1中給出了 prototype 鏈的示意圖。


圖 1. JavaScript prototype 鏈示意圖
JavaScript prototype鏈示例圖

圖 1中,studentA是通過 new 操作符創建的,因此它的 prototype 指向其構造器Student的 prototype ;Student.prototype的值是通過 new 操作符創建的,其 prototype 指向構造器Person的 prototype 。studentA的 prototype 鏈在圖 1中用虛線表示。

prototype 鏈在屬性查找過程中會起作用。當在一個對象中查找某個特定名稱的屬性時,會首先檢查該對象本身。如果找到的話,就返回該屬性的值;如果找不到的話,會檢查該對象的 prototype 指向的對象。如此下去直到找到該屬性,或是當前對象的 prototype 爲 null 。 prototype 鏈在設置屬性的值時並不起作用。當設置一個對象中某個屬性的值的時候,如果當前對象中存在這個屬性,則更新其值;否則就在當前對象中創建該屬性。

JavaScript 中並沒有 Java 或 C++ 中類(class)的概念,而是通過 prototype 鏈來實現基於 prototype 的繼承。在 Java 中,狀態包含在對象實例中,方法包含在類中,繼承只發生在結構和行爲上。而在 JavaScript 中,狀態和方法都包含在對象中,結構、行爲和狀態都是被繼承的。這裏需要注意的是 JavaScript 中的狀態也是被繼承的,也就是說,在構造器的 prototype 中的屬性是被所有的實例共享的。如代碼清單 4中所示。


清單 4. JavaScript 中狀態被繼承的示例
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中的問題在於將selectedCourses作爲 prototype 的屬性之後,studentAstudentB兩個實例共享了該屬性,它們操作的實際是同樣的數據。





回頁首


this

JavaScript 中的 this 一直是容易讓人誤用的,尤其對於熟悉 Java 的程序員來說,因爲 JavaScript 中的 this 與 Java 中的 this 有很大不同。在一個 function 的執行過程中,如果變量的前面加上了 this 作爲前綴的話,如this.myVal,對此變量的求值就從 this 所表示的對象開始。

this 的值取決於 function 被調用的方式,一共有四種,具體如下:

  • 如果一個 function 是一個對象的屬性,該 funtion 被調用的時候,this 的值是這個對象。如果 function 調用的表達式包含句點(.)或是 [],this 的值是句點(.)或是 [] 之前的對象。如myObj.funcmyObj["func"]中,func被調用時的 this 是myObj
  • 如果一個 function 不是作爲一個對象的屬性,那麼該 function 被調用的時候,this 的值是全局對象。當一個 function 中包含內部 function 的時候,如果不理解 this 的正確含義,很容易造成錯誤。這是由於內部 function 的 this 值與它外部的 function 的 this 值是不一樣的。代碼清單 5中,在myObjfunc中有個內部名爲inner的 function,在inner被調用的時候,this 的值是全局對象,因此找不到名爲myVal的變量。這個時候通常的解決辦法是將外部 function 的 this 值保存在一個變量中(此處爲self),在內部 function 中使用它來查找變量。
  • 如果在一個 function 之前使用 new 的話,會創建一個新的對象,該 funtion 也會被調用,而 this 的值是新創建的那個對象。如function User(name) {this.name = name}; var user1 = new User("Alex");中,通過調用new User("Alex"),會創建一個新的對象,以user1來引用,User這個 function 也會被調用,會在user1這個對象中設置名爲name的屬性,其值是Alex
  • 可以通過 function 的 apply 和 call 方法來指定它被調用的時候的 this 的值。 apply 和 call 的第一個參數都是要指定的 this 的值,兩者不同的是調用的實際參數在 apply 中是以數組的形式作爲第二個參數傳入的,而 call 中除了第一個參數之外的其它參數都是調用的實際參數。如func.apply(anotherObj, [arg1, arg2])中,func調用時候的 this 指的是anotherObj,兩個參數分別是arg1arg2。同樣的功能用 call 來寫則是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 中並沒有 Java 或是 C++ 中的類(class)的概念,而是採用構造器(constructor)的方式來創建對象。在 new 表達式中使用構造器就可以創建新的對象。由構造器創建出來的對象有一個隱含的引用指向該構造器的 prototype 。

所有的構造器都是對象,但並不是所有的對象都能成爲構造器。能作爲構造器的對象必須實現隱含的[[Construct]方法。如果 new 操作符後面的對象並不是構造器的話,會拋出 TypeError 異常。

new 操作符會影響 function 調用中 return 語句的行爲。當 function 調用的時候有 new 作爲前綴,如果 return 的結果不是一個對象,那麼新創建的對象將會被返回。在代碼清單 6中,functionanotherUser中通過 return 語句返回了一個對象,因此u2引用的是返回的那個對象;而 functionuser並沒有使用 return 語句,因此u1引用的是新創建的user對象。


清單 6. new 操作符對 return 語句行爲的影響
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 可以用來解釋執行一段 JavaScript 程序。當傳給 eval 的參數的值是字符串的時候,該字符串會被當成一段 JavaScript 程序來執行。

隱式的 eval

除了顯式的調用 eval 之外,JavaScript 中的有些 function 能接受字符形式的 JavaScript 代碼並執行,這相當於隱式的調用了 eval 。這些 function 的典型代表是setTimeoutsetInterval。具體請見代碼清單 7。由於 eval 的性能比較差,所以在使用setTimeoutsetInterval等 function 的時候,最好傳入 function 的引用,而不是字符串。


清單 7. 隱式的 eval 示例
var obj = { 
    show1 : function() { alert(" 時間到! "); }, 
    show2 : function() { alert("10 秒一次的提醒! "); }; 
 }; 

 setTimeout(obj.show1, 1000); 
 setTimeout("obj.show1();", 2000); 

 setInterval(obj.show2, 10000); 
 setInterval("obj.show2();", 10000);

eval 的潛在危險

在目前的 Ajax 應用中,JSON 是一種流行的瀏覽器端和服務器端處之間傳輸數據的格式。服務器端傳過來的數據在瀏覽器端通過 JavaScript 的 eval 方法轉換成可以直接使用的對象。然而,在瀏覽器端執行任意的 JavaScript 會帶來潛在的安全風險,惡意的 JavaScript 代碼可能會破壞應用。對於這個問題,有兩種解決方式:

帶註釋的 JSON(JSON comments filtering)和帶前綴的 JSON(JSON prefixing)
這兩種方法都是 Dojo 中用來避免 JSON 劫持(JSON hijacking)的。帶註釋的 JSON 指的是從服務器端返回的 JSON 數據都是帶有註釋的,瀏覽器端的 JavaScript 代碼需要先去掉註釋的標記,再通過 eval 來獲得 JSON 數據。這種方法一度被廣泛使用,後來被證明並不安全,還會引入其它的安全漏洞。帶前綴的 JSON 是目前推薦使用的方法,這種方法的使用非常簡單,只需要在從服務器端的 JSON 字符串之前加上{} &&,再調用 eval 。關於這兩種方法的細節,請看參考資料
對 JSON 字符串進行語法檢查
安全的 JSON 應該是不包含賦值和方法調用的。在 JSON 的 RFC 4627 中,給出了判斷 JSON 字符串是否安全的方法,是通過兩個正則表達式來實現的。具體見代碼清單 8。關於 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 規範(請看參考資料)中用來描述 JavaScript 代碼執行的抽象概念。所有的 JavaScript 代碼都是在某個執行上下文中運行的。在當前執行上下文中調用 function 的時候,會進入一個新的執行上下文。當該 function 調用結束的時候,會返回到原來的執行上下文中。如果 function 調用過程中拋出異常,並沒有被捕獲的話,有可能從多個執行上下文中退出。在 function 調用過程,也可能調用其它的 function,從而進入新的執行上下文。由此形成一個執行上下文棧。

每個執行上下文都與一個作用域鏈(scope chain)關聯起來。該作用域鏈用來在 function 執行時求標識符(Identifier)的值。在該鏈中包含多個對象。在對標識符進行求值的過程中,會從鏈首的對象開始,然後依次查找後面的對象,直到在某個對象中找到與標識符名稱相同的屬性。如”protype 鏈與繼承“中所述,在每個對象中進行屬性查找的時候,會使用該對象的 prototype 鏈。在一個執行上下文中,與其關聯的作用域鏈只會被with語句和 catch 子句影響。

在進入一個新的執行上下文的時候,會按順序執行下面的操作:

  • 創建激活(Activation)對象
    激活對象是在進入新的執行上下文的時候被創建出來的,並與新的執行上下文關聯起來。在初始化的時候,該對象包含一個名爲arguments的屬性。激活對象在變量初始化的時候也會被使用。 JavaScript 代碼不能直接訪問該對象,但是可以訪問該對象裏面的成員(如 arguments)。
  • 創建作用域鏈
    接下來的操作是創建作用域鏈。每個 function 都有一個內部屬性[[scope]],它的值是一個包含多個對象的鏈。該屬性的具體值與 function 的創建方式和在代碼中的位置有很大關係(見“function 對象的創建方式”)。這個步驟中的主要操作是將上一步中創建的激活對象添加到 function 的[[scope]]屬性對應的鏈的前面。
  • 變量初始化
    該步驟對 function 中需要使用的變量進行初始化。初始化時使用的對象是第一步中所創建的激活對象,不過被稱之爲變量(Variable)對象。會被初始化的變量包括 function 調用時的實際參數、內部 function 和局部變量。在這個步驟中,對於局部變量,只是在變量對象中創建了同名的屬性,但是屬性的值爲 undefined,只有在 function 執行過程中才會被真正賦值。
全局 JavaScript 代碼是在全局執行上下文中運行的,該上下文的作用域鏈只包含一個全局對象。

圖 2中給出了 function 執行過程中的作用域鏈的示意圖,其中的虛線表示作用域鏈。


圖 2. 作用域鏈示意圖
作用域鏈示意圖




回頁首


function a() {}、var a = function() {} 與 var a = new Function()

在 JavaScript 中,function 對象的創建方式有三種:function 聲明、function 表達式和使用 Function 構造器。通過這三種方法創建出來的 function 對象的[[scope]]屬性的值會有所不同,從而影響 function 執行過程中的作用域鏈。下面具體討論這三種情況。

function 聲明
function 聲明的格式是function funcName() {}。使用 function 聲明的 function 對象是在進入執行上下文時的變量初始化過程中創建的。該對象的[[scope]]屬性的值是它被創建時的執行上下文對應的作用域鏈。
function 表達式
function 表達式的格式是var funcName = function() {}。使用 function 表達式的 function 對象是在該表達式被執行的時候創建的。該對象的[[scope]]屬性的值與使用 function 聲明創建的對象一樣。
Function 構造器
對於 Function 構造器,大家可能比較陌生。聲明一個 function 時,通常使用前兩種方式。該方式的格式是var funcName = new Function(p1, p2,..., pn, body),其中 p1、p2 到 pn 表示的是該 function 的形式參數,body 是 function 的內容。使用該方式的 function 對象是在構造器被調用的時候創建的。該對象的[[scope]]屬性的值總是一個只包含全局對象的作用域鏈。
function 對象的 length 屬性可以用來獲取聲明 function 時候指定的形式參數的個數。如前所述,function 對象被調用時的實際參數是通過 arguments 來獲取的。





回頁首


with

with 語句的語法是with ( Expression ) Statement。 with 會把由 Expression 計算出來的對象添加到當前執行上下文的作用域鏈的前面,然後使用這個擴大的作用域鏈來執行語句 Statement,最後恢復作用域鏈。不管裏面的語句是否正常退出,作用域鏈都會被恢復。

由於 with 語言會把額外的對象添加到作用域鏈的前面,使用 with 可能會影響性能並造成難以發現的錯誤。由於額外的對象在作用域鏈的前面,當執行到 with 裏面的語句,需要對一個標識符求值的時候,會首先沿着該對象的 prototype 鏈查找。如果找不到,纔會依次查找作用域鏈中原來的對象。因此,如果在 with 裏面的語句中頻繁引用不在額外對象的 prototype 鏈中的變量的話,查找的速度會比不使用 with 慢。具體見代碼清單 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" 
 })();

在代碼中,首先通過 prototype 的方式實現了繼承。在 with 中,執行alert(typeof a)需要查找變量 a,由於 obj 在作用域鏈的前面,而 obj 中也存在名爲 a 的屬性,因此 obj 中的 a 被找到。執行alert(myVar)需要查找變量 myVal,而 obj 中不存在名爲 myVal 的屬性,會繼續查找作用域鏈中後面的對象,因此比不使用 with 的速度慢。需要注意的是最後一條語句alert(typeof a),它不在 with 裏面,因此查找到的 a 是之前聲明的 number 型的變量。





回頁首


閉包

閉包(closure)是 JavaScript 中一個非常強大的功能。如果使用得當的話,可以使得代碼更簡潔,並實現在其它語言中很難實現的功能;而如果使用不當的話,則會導致難以調試的錯誤,也可能造成內存泄露。只有在充分理解閉包的基礎上,才能正確的使用它。理解閉包需要首先理解 JavaScript 中的prototype 鏈執行上下文和作用域鏈等概念。

閉包指的是一個表達式(通常是一個 function),該表達式可以有自由的變量,並且運行環境能夠正確的獲取這些變量的值。 JavaScript 中閉包的產生是由於 JavaScript 中允許內部 function,也就是在一個 function 內部聲明的 function 。內部 function 可以訪問外部 function 中的局部變量、傳入的參數和其它內部 function 。當內部 function 可以在包含它的外部 function 之外被引用時,就形成了一個閉包。這個時候,即便外部 function 已經執行完成,該內部 function 仍然可以被執行,並且其中所用到的外部 function 的局部變量、傳入的參數等仍然保留外部 function 執行結束時的值。

下面通過一個例子來說明閉包的形成,見代碼清單 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的調用者,同時add在方法體中使用了addBy的參數first。這樣就形成了一個閉包。通過調用addBy(10)得到的 functionfunc,在其之後的執行過程中,都會保留創建的時候使用的first參數的值10

下面分析代碼清單 10中執行的細節。首先addBy(10)被調用。由於addBy是在全局代碼中聲明的,因此被調用時候的執行上下文對應的作用域鏈只包含全局對象。在addBy的方法體中,聲明瞭一個內部 functionaddadd[[scope]]屬性會在作用域鏈之前加上 functionaddBy的激活對象。該對象中包含了經過初始化的參數first,其值爲10。至此,functionfunc[[scope]]屬性的值是包含兩個對象。當func被調用的時候,會進入一個新的執行上下文,而此時的作用域鏈的前面加上了 functionadd調用時的激活對象。該對象中包含了經過初始化的參數second,其值爲20。在func的執行過程中,需要對兩個標識符firstsecond求值的時候,會使用之前提到的包含三個對象的作用域鏈。從而可以正確的求值。

在 JavaScript 中,正確的使用閉包可以簡化代碼。下面舉幾個例子來說明。

避免名稱空間衝突

在多人協作開發應用,或是使用第三方開發的 JavaScript 庫的時候,一個通常會遇到的問題是名稱空間衝突。比如第三方的 JavaScript 庫在全局對象中聲明瞭一個屬性叫test,如果在自己的代碼中也會聲明同樣名稱的屬性的話,當兩者一同使用的時候,後加載的屬性值會替換之前的值,從而造成錯誤。

這個時候典型的做法是隻在全局對象中保存一個對象,所有的功能都通過引用此對象來完成。完成功能所需要的內部狀態都封裝在一個閉包中。如代碼清單 11所示。


清單 11. 使用閉包避免名稱空間衝突
(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

代碼中通過創建一個匿名 function 並立即執行來生成一個閉包。在閉包中,通過修改全局對象MyCode來添加所需的功能。內部狀態之一的屬性defaultName被封裝在閉包中,不能被閉包之外的代碼所引用,也不會引發命名衝突。

保存狀態

在 JavaScript 代碼運行過程中,不可避免的需要保存一些內部狀態。通過使用閉包,可以將內部狀態封裝在一個 function 內部,使得代碼更加簡潔。如代碼清單 12所示。


清單 12. 使用閉包保存狀態
var getNextId = (function() { 
  var id = 1; 
  return function() { 
    return id++; 
  } 
 })(); 

 getNextId();  // 輸出 1 
 getNextId();  // 輸出 2 
 getNextId();  // 輸出 3

代碼中的getNextId的功能是生成惟一的 ID,因此它需要維護當前的 ID 這樣一個狀態。通過使用閉包,不需要在全局對象中添加一個新的屬性,該屬性由閉包來維護。閉包之外的代碼也不能訪問或修改getNextId的內部狀態。

摺疊調用參數

在 JavaScript 中,有些 function,如setTimeoutsetInterval,只接受一個 function 作爲參數。在有些情況下,這些 function 的執行是需要額外的參數的。這個時候可以通過使用閉包,將原始 function 的參數進行摺疊,得到一個沒有參數的新 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需要三個參數來完成其功能。如果直接將doSomething傳給setTimeout的話,三個參數的值都是 undefined 。fold將三個參數的值保存在激活對象,並添加在作用域鏈中。這樣即便返回的 function 是沒有參數的,它仍然可以獲得這三個參數的值。

關於閉包的更多內容,請參見參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章