天天看點

轉 跨越邊界: JavaScript 語言特性

JavaScript 常被人們認為是程式設計語言中無足輕重的一員。這種觀點的形成可以“歸功”于其開發工具、複雜且不一緻的面向 HTML 頁面的 文檔對象模型以及不一緻的浏覽器實作。但 JavaScript 絕對不僅僅是一個玩具這麼簡單。在本文中,Bruce Tate 向您介紹了  JavaScript 的語言特性。 幾乎每個 Web 開發人員都曾有過詛咒 JavaScript 的經曆。這個備受争議的語言受累于其複雜的稱為 文檔對象模型 (DOM)的程式設計模型、糟糕的實作和調試工具以及不一緻的浏覽器實作。直到最近,很多開發人員還認為 Javascript 從最好的方面 說是無可避免之災禍,從最壞的方面說不過是一種玩具罷了。 

然而 JavaScript 現在開始日益重要起來,而且成為了廣泛應用于 Web 開發的腳本語言。JavaScript 的複蘇使一些業界領袖 人物也不得不開始重新審視這種程式設計語言。諸如 Ajax (Asynchronous JavaScript + XML) 這樣的程式設計技術讓 Web  網頁更加迷人。而完整的 Web 開發架構,比如 Apache Cocoon,則讓 JavaScript 的應用越來越多,使其不隻限于是一種用于制 作 Web 頁面的簡單腳本。JavaScript 的一種稱為 ActionScript 的派生物也推動了 Macromedia 的 Flash  用戶端架構的發展。運作在 JVM 上的實作 Rhino 讓 JavaScript 成為了 Java™ 開發人員所首選的一類腳本語言(參見 參考資料)。 

我的好友兼同僚 Stuart Halloway 是 Ajax 方面的專家,曾在其教授的 JavaScript 課程中做過這樣的開場白:“到  2011 年,JavaScript 将被公認為是一種擁有開發現代應用程式所需的一整套新特性的語言” 。他繼而介紹說 JavaScript 程式 要比類似的 Java 程式緊密十倍,并繼續展示了使其之是以如此的一些語言特性。 

轉 跨越邊界: JavaScript 語言特性

關于這個系列

在 跨越邊界系列中, 作者 Bruce Tate 提出了這樣一個主張:今天的 Java 程式員通過學習其他技術和語言,會得到很好的幫助。程式設計領域已經發生了變化, Java 技術不再是所有開發項目理所當然的最佳選擇。其他架構正在影響 Java 架構建構的方式,而從其他語言學到的概念也有助于 Java 程式設計。 對 Python(或 Ruby、Smalltalk 等等)代碼的編寫可能改變 Java 編碼的方式。

這 個系列介紹的程式設計概念和技術,與 Java 開發有根本的不同,但可以直接應用于 Java 程式設計。在某些情況下,需要內建這些技術來利用它們。在其他情 況下,可以直接應用這些概念。單獨的工具并不重要,重要的是其他語言和架構可以影響 Java 社群中的開發人員、架構,甚至是基本方式。 

在這篇文章中,我将帶您探究 JavaScript 的一些特性,看看這些特性如何讓它如此具有吸引力: 

  • 高階函數: 一個高階函數可以将函數作為參數,也可以傳回一個函數。此特性讓 JavaScript 程式員可以用 Java 語言所不能提供的方法來操縱函數。
  • 動态類型:通過延遲綁定,JavaScript 可以更準确和更靈活。
  • 靈活的對象模型:JavaScript 的對象模型使用一種相對不常見的方式進行繼承 —— 稱為原型 —— 而不是 Java 語言中更常見的基于類的對象模型。  

您可能已經熟悉動态類型模型、高階函數形式的函數式程式設計以及開放對象模型這些概念,因為我在其他的跨越邊界  系列文章中已經作過相關的介紹。如果您從未進行過任何正式的 JavaScript 開發,您很可能會認為這些特性屬于非常複雜的語言,例如  Python、Lisp、Smalltalk 和 Haskell,而絕非像 JavaScript 這樣的語言所能提供的。是以,我将用實際的代碼示 例來說明這些概念。 

立即開始

您無需設定 JavaScript。如果您可以在浏覽器中閱讀此篇文章,就證明您已經準備就緒了。本文包含的所有程式設計示例都可以在大多數浏覽器内運作。我使用的是 Firefox。 

用在 

<script type='text/javascript'>

 和 

</script>

 标記之間所包含的 JavaScript 加載簡單的 Web 頁面。清單 1 可以顯示 Hello, World 文本: 

清單 1. Hello, world

    
<script type='text/javascript'>
alert('Hello, World.')
</script>
      

要運作此代碼,隻需建立一個名為 example1.html 的檔案。将清單 1 的代碼複制到該檔案内,并在浏覽器中加載此檔案(參看 下載下傳 部分以獲得本文使用的所有示例 HTML 檔案)。注意到每次重載此頁面時,該代碼都會立即執行。

alert

 是個函數調用,隻有一個字元串作為參數。圖 1 顯示了 由清單 1 中的代碼彈出的警告框,顯示文本 “Hello, World”。如果代碼在 HTML body 之内(目前并未指定任何 body,但浏 覽器能接受不規則的 HTML,并且整個頁面都默然作為一個 body 被處理)。頁面一旦加載,JavaScript 就會立即執行。 

圖 1. Hello, world

轉 跨越邊界: JavaScript 語言特性

如果要延遲執行,可以在 HTML 

<head>

 元素聲明 JavaScript 函數,如清單 2 所示: 

清單 2. 延遲執行

js 代碼

<head>

    <script type='text/javascript'>

        function hello() {

            alert('Hello, World.')

        }

    </script>

</head>

<body>

    <button οnclick="hello();">Say Hello</button>

</body>

将清單 2 中的代碼輸入到一個 HTML 檔案,在浏覽器内加載該檔案,單擊 Say Hello 按鈕,結果如圖 2 所示:

圖 2. 延遲執行

轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
回頁首

高階函數

從 清單 2,可以大緻體會到一些 JavaScript 在操縱函數方面的能力。将函數名稱傳遞給 HTML 

button

 标記并利用 HTML 的内置事件模型。使用 JavaScript 時,我會經常在變量或數組中存儲函數(在本文後面的 對象模型 一節,您會看到 JavaScript 對象模型政策大量使用了此技巧)。例如,檢視一下清單 3: 

清單 3. 用變量操縱函數

    
<head>
    
    <script type='text/javascript'>
        hot = function hot() {
            alert('Sweat.')
        }
        cold  = function cold() {
            alert('Shiver.')
        }
        
        function swap() {
            temp = hot
            hot = cold
            cold = temp    
            alert('Swapped.')
        }
    </script>
</head>
<body>
    <button οnclick="hot();">Hot</button>
    <button οnclick="cold();">Cold</button>
    <button οnclick="swap();">Swap</button>
</body>
      

函數是 JavaScript 中的一類對象,可以自由地操縱它們。首先我聲明兩個函數:

hot

 和 

cold

。并分别在不同的變量存儲它們。單擊 Hot 或 Cold 按鈕會調用對應的函數,生成一個告警。接下來,聲明另一個函數用來交換 Hot 和 Cold 按鈕的值,将此函數與第三個按鈕關聯,該按鈕顯示如圖 3 所示的告警: 

圖 3. 操縱函數

轉 跨越邊界: JavaScript 語言特性

這個例子說明可以像處理其他變量一樣處理函數。C 開發人員很容易将此概念看作是函數指針 功能,但 JavaScript 的高階函數的功能更為強大。該特性讓 JavaScript 程式員能夠像處理其他變量類型一樣輕松處理動作或函數。

将函數用作函數的參數,或将函數作為值傳回,這些概念屬于高階函數的領域。清單 4 對 清單 3 做了一點點修改,顯示了能傳回函數的高階函數:

清單 4. 高階函數

    
<head>

    <script type='text/javascript'>

        function temperature() {
            return current
        }

        hot = function hot() {
            alert('Hot.')
        }

        cold  = function cold() {
            alert('Cold.')
        }

        current = hot

        function swap() {
            if(current == hot) {
              current = cold
            } else {
              current = hot
            }
        }
    </script>
</head>
<body>
    <button οnclick="funct = temperature()();">Temperature</button>
    <button οnclick="swap();">Swap</button>
</body>
      

這個例子解決了一個常見問題:如何将更改中的行為附加到使用者接口事件?通過高階函數,這很容易做到。

temperature

 高階函數傳回 

current

 的值,而 current 又可以有 

hot

 或 

cold

 函數。看一下這個有些陳舊的函數調用:

temperature()()

。第一組括号用于調用 

temperature

 函數。第二組括号調用由 

temperature

 傳回 的函數。圖 4 顯示了輸出:

圖 4. 高階函數

轉 跨越邊界: JavaScript 語言特性

高階函數是函數式程式設計的基礎,對比面向對象程式設計,函數式程式設計代表了更進階别的抽象。但 JavaScript 的實力并不僅限于高階函數。JavaScript 的動态類型就極為适合 UI 開發。

轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
回頁首

動态類型

通過靜态類型,編譯器可以檢查參數和變量的值或針對一個給定操作所允許的傳回值。其優勢是編譯器可以做額外的錯誤檢查。而且靜态類型還可以為諸如 IDE 這樣的工具提供更多資訊,帶來其他一些特性,比如更好的代碼完成功能。但靜态類型也存在着如下一些劣勢:

  • 必須提前聲明意圖,這常常會導緻靈活性降低。例如,更改一個 Java 類就會更改類的類型,因而必須重新編譯。對比之下,Ruby 允許開放的類,但更改一個 Java 類還是會更改類的類型。
  • 要實作相同的功能,必須輸入更多的代碼。例如,必須用參數形式包括進類型資訊,必須用函數形式傳回值和所有變量的類型。另外,還必須聲明所有變量并顯式地轉化類型。
  • 靜态語言的編譯-部署周期要比動态語言的部署周期長,盡管一些工具可被用來在某種程度上緩解這一問題。  

靜态類型更适合用于建構中間件或作業系統的語言中。UI 開發常常需要更高的效率和靈活性,是以更适合采用動态類型。我深知 這種做法存在危險。相信使用過 JavaScript 的 Web 開發人員都曾經為編譯器本應檢測到的錯誤類型的變量而絞盡腦汁。但它所帶來的優勢同樣 不可否認。下面将舉例加以說明。

首先,考慮一個對象的情況。在清單 5 中,建立一個新對象,并通路一個不存在的屬性,名為 

color

清單 5. 引入一個屬性

    
<script type='text/javascript'>
    blank_object = new Object();
    blank_object.color = 'blue'
    alert('The color is ' + blank_object.color)
</script>
      

當加載并執行此應用程式時,會得到如圖 5 所示的結果:

圖 5. 引入屬性

轉 跨越邊界: JavaScript 語言特性

JavaScript 并不會報告 

blue

 屬性不存在的錯誤。靜 态類型的擁護者大都會被本例所吓倒,因為本例中的錯誤被很好地隐匿了。雖然這種做法多少會讓您感覺有些不正當,但您也不能否認它巨大的誘惑力。您可以很快 引入屬性。如果将本例和本文之前的例子結合起來,還可以引入行為。記住,變量可以儲存函數!是以,基于動态類型和高階函數,您可以在任何時候向類中引入任 意的行為。 

可以輕松地重寫 清單 5,使其如清單 6 所示:

清單 6. 引入行為

    
<script type='text/javascript'>
    blank_object = new Object();
    blank_object.color = function() { return 'blue'}
    alert('The color is ' + blank_object.color())
</script>
      

從上例可以看出,在 JavaScript 的不同概念之間可以如此輕松地來回變換,其含義上的變化很大 —— 比如,是引入行為還是引入資料  —— 但文法上的變化卻很小。該語言很好的延展性是它的一種優勢,但同樣也是其缺點所在。實際上,該語言本身的對象模型就是 JavaScript 延展 程度的一種展現。

轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
回頁首

對象模型

到目前為止,您應該對 JavaScript 有一個正确的評價了,它絕非隻如一個玩具那麼簡單。事實上,很多人都使用過其對象模型建立過極為複雜、設計良好的面向對象軟體。但對象模型尤其是用于繼承的對象模型又非您一貫認為的那樣。 

Java 語言是基于類的。當建構應用程式時,也同時建構了可以作為所有對象的模闆的新類。然後調用 

new

 來執行個體化該模闆,建立一個新對象。而在 JavaScript 中,所建立的是一個原型,此原型是一個執行個體,可以建立所有未來的對象。 

現在先暫且放下這些抽象的概念,去檢視一些實際代碼。比如,清單 7 建立了一個簡單的 

Animal

,它具有 

name

 屬性和 

speak

 動作。其他動物會從這個基礎繼承。 

清單 7. 建立一個構造函數

    
<script type='text/javascript'>        
Animal = function() {
    this.name = "nobody"
    this.speak = function () {
        return "Who am I?"
    }
}

myAnimal = new Animal();
alert('The animal named ' + myAnimal.name + 
      ' says ' + myAnimal.speak());

</script>
      

清單 7 的結果如圖 6 所示:

圖 6. 建立一個構造函數

轉 跨越邊界: JavaScript 語言特性

對于 Java 開發人員而言,清單 7 中的代碼看起來多少有點生疏和奇怪。實際上對于沒有親自建構過對象的許多 JavaScript 開發人員來說,這些代碼同樣看起來有點生疏和奇怪。也許,下面的解釋可以讓大家能夠更好地了解這段代碼。 

實際上,您隻需重點關注其中三段資訊。首先,JavaScript 用嵌套函數表示對象。這意味着清單 7 中的 

Animal

 的定義是一種有效的文法。第二,JavaScript 基于原型或現有的對象的執行個體來構造對象,而非基于類模闆。

funct()

 是一種調用,但 

new Animal()

 卻基于 

Animal

 内的原型構造一個對象。最後,在 JavaScript 中,對象隻是函數和變量的集合。每個對象并不與類型相關,是以可以自由地修改這種結構。 

回到 清單 7。如您所見,JavaScript 基于在 

Animal

 中指定的原型定義一個新對象:

myAnimal

。繼而可以使用原型中的屬性和函數,甚或重定義函數和屬性。這種靈活性可能會讓 Java 開發人員受不了,因為他們不習慣這種行為,但它的确是一種十分強大的模型。

現在我還要更深入一步。您還可以使用名為 

prototype

 執行個體變量來指定對象的基礎。方法是設定 

prototype

 執行個體變量使其指向繼承鍊的父。如此設定 

prototype

 之後,您所建立的對象會為未指定的那些對象繼承屬性和函數。這樣一來,您就可以模仿面向對象的繼承概念。以清單 8 為例: 

清單 8. 通過原型繼承

    
<script type='text/javascript'>        

Animal = function() {
    this.name = "nobody"
    this.speak = function () {
        return "Who am I?"
    }
}
Dog = function() {
  this.speak = function() {
    return "Woof!"
  }
}
Dog.prototype = new Animal();

myAnimal = new Dog();
alert('The animal named ' + myAnimal.name + 
      ' says ' + myAnimal.speak());
      </script>
      

在清單 8 中,建立了一個 

Dog

 原型。此原型基于 

Animal

Dog

 重定義 

speak()

 方法但卻不會對 

name()

 方法做任何改動。随後,将原型 

Dog

 設定成 

Animal

。圖 7 顯示了其結果:

圖 7. 通過原型繼承

轉 跨越邊界: JavaScript 語言特性

這也展示了 JavaScript 是如何解決到屬性或方法的引用問題的:

  • JavaScript 基于原始的原型建立執行個體,該原型在構造函數中定義。任何對方法或屬性的引用都會使用所生成的原始副本。
  • 您可以在對象内像定義其他任何變量一樣重新定義這些變量。這樣做必然會更改此對象。是以您顯式定義的任何屬性或函數都将比在原始的原型中定義的那些屬性或函數優先級要高。
  • 如果您顯式設定了名為 

    prototype

     的執行個體變量,JavaScript 就會在此執行個體中尋找任何未定義的執行個體變量或屬性。這種查找是遞歸的:如果 在 

    prototype

     内定義的執行個體不能找到屬性或函數,它就會在其 原型中查找,依此類推。  

那麼,JavaScript 的繼承模型到底是什麼樣的?這取決于您如何對它進行定義。您需要定義繼承行為以便可以覆寫它。 然而,從本質上講,JavaScript 更像是一種函數式語言,而非面向對象的語言,它使用一些智能的文法和語義來仿真高度複雜的行為。其對象模型極為 靈活、開放和強大,具有全部的反射性。有些人可能會說它太過靈活。而我的忠告則是,按具體作業的需要選擇合适的工具。

轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
轉 跨越邊界: JavaScript 語言特性
回頁首

結束語

JavaScript 對象模型建構在該語言的其他功能之上來支援大量的庫,比如 Dojo(參見 參考資料)。這種靈活性讓每個架構能夠以一種精細的方式更改對象模型。在某種程度上,這種靈活性是一種極大的缺點。它可以導緻可怕的互操作性問題(盡管該語言的靈活性可以部分緩解這些問題)。 

而另一方面,靈活性又是一種巨大的優勢。Java 語言一直苦于無法充分增強其靈活性,原因是它的基本對象模型還未靈活到可以被擴充的程度。一個典 型的企業級開發人員為能夠成功使用 Java 語言必須要學習很多東西,而新出現的一些優秀的開放源碼項目和新技術,比如面向方面程式設計、Spring 編 程架構和位元組碼增強庫,則帶來了大量要學的代碼。 

最後,JavaScript 優秀的靈活性的确讓您體會到了一些高階語言的強大功能。當然您無需選擇為每個項目或大多數項目都做這樣的權衡和折衷。 但了解一種語言的優勢和劣勢 —— 通過參考大量資訊,而不僅僅基于廣告宣傳或公衆意見 —— 會讓您可以更好地控制何時需要使用以及何時不能使用這種語 言。當您在修改 JavaScript Web 小部件時,您至少知道該如何讓此語言發揮它最大的優勢。請繼續跨越邊界吧。

繼續閱讀