為現代 JavaScript 開發做好準備

edithfang發表於2014-05-15

這篇文章將會介紹一些很基礎的JS知識,以及當開發者想要嘗試Backbone.js和Ember.js之類的工具之前需要知道一些內容。當你理解了這篇文章中的大部分內容的時候,你會更有信心去學習其他高階JavaScript知識的時候。這篇文章是假設你曾經使用過JavaScript的,所以如果你從沒有接觸過它,也許你需要先了解下更基礎的知識。現在我們開始吧!

模組

有多少人在一個檔案中寫的JS像下面的程式碼塊一樣?(注意:我可沒有說內嵌在HTML檔案中哦):

var someSharedValue = 10;
var myFunction = function(){ //do something }
var anotherImportantFunction = function() { //do more stuff }

如果你做到了這一點,那麼很有可能你正在寫這樣的程式碼。我不是在給你下定義,因為在相當長的一段時間裡我也曾這麼寫程式。事實上這段程式碼有很多毛病,不過我們會專注在討論全域性名稱空間的汙染問題上。這樣的程式碼程式碼會把方法和變數都暴露在了全域性中,我們需要將讓這些資料與全域性名稱空間獨立開來,我們將會採用模組模式(Module Pattern)來實現這個目的。模組中可以有很多不同的形式達到我們的目標,我會從最簡單的方法開始說:匿名函式(Immediately Invoked Function Expression,簡寫為:IIFE)。

名字聽起來很高大上,不過它的實現其實很簡單:

(function(){
    //do some work
})();

如果在此之前你從未接觸過匿名函式,可能現在你會覺得它很怪 — 怎麼會有這麼多括號!匿名函式是會立即執行的函式,你可以這麼理解:一個函式被建立了後又立刻被呼叫。它應該是一個表達而不是一個語句:一個函式語句是一定要有一個名字的,但是大家也看到了,匿名函式是沒有名字的。在函式定義的外部還有一組括號,這一點也能很好地幫助我們在程式碼中輕易找到匿名函式的身影。

現在我們知道要怎麼寫一個匿名函式了,那就來聊聊為什麼要使用它吧。在JS中我們都是在和各種作用域之中的函式打交道,所以如果我們想要建立一個作用域,就可以使用函式。匿名函式中的變數和方法的作用域僅僅在匿名函式中,就不會汙染全域性的名稱空間,那麼現在還需要考慮的一個問題是,我們要如何從外部取得那些在匿名函式作用域中的變數和方法呢?答案就是全域性名稱空間:將變數放入全域性名稱空間中,或者至少將作用變數與全域性名稱空間關聯起來

想要在匿名函式外部呼叫方法,我們可以將window物件傳入匿名函式,再將函式或變數值賦值到這個物件上。為了保證這個window物件的引入不會造成什麼混亂,我們可以將widow物件作為一個變數傳入我們的匿名函式。當做函式傳入引數的方法同樣適用於第三方庫,甚至undefined這樣的值。現在我們的匿名函式看起來是這樣的:

(function(window, $, undefined){
    //do some work
})(window, jQuery);

正如你所看到的,我們將window和jQuery傳入函式中(’$'符號表示的就是’jQuery’,把它用在這的原因是防止其他庫也定義了’$'),但是這個函式其實是接收了3個引數。如果我們沒有傳入第三個引數,那麼在遇到undefined的時候就會結束, 為了避免有其他的JS檔案更改這一點,所以我們將一個undefined的變數傳入方法中來保證這個方法裡是一定可以使用undefined的。其實在函式內我們也是可以直接使用這些值,能這麼做的原理是,JS的閉包會覆蓋他們所處的上下文。對於這個話題,我曾寫過一篇關於C#的文章以解釋這個概念,這兩者是互通的。

現在我們有了一個會立即執行的方法,還有一個相對安全的執行上下文,其中還包含有window$undefined變數(這幾個變數還是有可能在這個指令碼被執行前就被重新賦值,不過現在的可能性要小的多了)。現在我們已經做得很好了:把我們的程式碼從全域性環境下的一團混亂的局面中拯救了出來;降低了與其他在同一應用中使用的指令碼的衝突可能性。

任何我們想要從模組中獲取的東西都可以通過window物件拿到。但是通常我不會直接將模組中的內容直接複製到window物件上,而是會用更有組織性地將模組中的內容。在大部分語言中,我們將這些容器稱為“名稱空間”,在JS中我們可以用“物件”的方式來模擬。

名稱空間

如果我們想要宣告一個名稱空間,將一個函式放進這個空間中,程式碼可以寫成這樣:

window.myApp = window.myApp || {};
window.myApp.someFunction = function(){
    //so some work
};

我們只是在全域性環境中建立了一個用於檢視某個物件是否已經存在,如果已經存在了,那麼我們就可以直接使用;不然就需要用’{}’來建立一個新的物件。接著,我們可以開始新增這個名稱空間的內容,將各種函式放入這個空間中,就像上面的程式碼片段所做的那樣,但是我們又不希望這些函式就隨便的放在那裡,而是希望將模組和名稱空間聯絡在一起,就像下面這樣:

(function(myApp, $, undefined){
    //do some work
}(window.myApp = window.myApp || {}, jQuery));

還可以這麼寫:

window.myApp = (function(myApp, $, undefined){
    //do some work
    return myApp;
})(window.myApp || {}, jQuery);

現在,我們不再是將window傳入我們的模組中,我們將一個和window物件聯絡在一起的名稱空間傳入模組中。之所以使用’||’的原因是我們可以重複使用同一個名稱空間,而不是每次需要使用名稱空間的時候我們又要重新建立一個。許多包含有名稱空間方法的庫會幫你建立好空間的,或者你可以使用一些想namespace.js這樣的工具來構建巢狀的名稱空間。由於在JS中,每一個在名稱空間中的項你都不得不指定它的名稱空間,所以通常我都儘量不會去建立深度巢狀的名稱空間。如果你在MyApp.MyModule.MySubModule中建立了一個doSomething方法,你需要這麼引用它:

MyApp.MyModule.MySubModule.doSomething();

每次你要呼叫它,或者你可以在你的模組中給這個名稱空間一個別名:

var MySubModule = MyApp.MyModule.MySubModule;

這樣定義以後,如果你想用doSomething這個方法可以用MySubModule.doSomething()來呼叫。不過這個方式其實是不必要的,除非你有非常非常多的程式碼,不然這麼做只會將問題複雜化。

揭祕模組模式

在建立模組時你也常會看到另一種設計模式:揭祕模組模式(Revealing Module Pattern)。它和模組模式有一些不同:所有定義在模組中的內容都是私有的,然後你可以把所有要暴露到模組外部的內容放在一個物件中,再返回這個物件。你可以這麼做:

var myModule = (function($, undefined){
     var myVar1 = '',
     myVar2 = '';
 
     var someFunction = function(){
         return myVar1 + " " + myVar2;
     };
 
     return {
         getMyVar1: function() { return myVar1; }, //myVar1 public getter
         setMyVar1: function(val) { myVar1 = val; }, //myVar1 public setter
         someFunction: someFunction //some function made public
     }
})(jQuery);

一次就建立一個模組,然後返回一個包含有需要公有化的模組片段的物件,同時模組中需要保持私有的變數也不會被暴露。myModule變數會包含有兩個共有的項,不過其中Somefunction中的myVar2是從外部獲取不到的。

建立構造器(類)

在JS中沒有“類”這個概念,但是我們可以通過建立構造器來建立“物件”。假設現在我們要建立一系列Person物件,還需要傳入姓、名和年齡,我們可以將構造器定義成下面這樣(這部分程式碼應該放在模組之中):

var Person = function(firstName, lastName, age){
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
}
 
Person.prototype.fullName = function(){
    return this.firstName + " " + this.lastName;
};

現在先看第一個函式,你會看到我們建立了一個Person構造器。我們用它來構造新的person物件。這個構造器需要3個傳入引數,然後將這3個引數賦值到執行上下文中。我們也是通過這種方式獲取到公有例項變數。這裡也可以建立私有變數:將傳入引數賦值到這個構造器中的區域性變數。但是這麼做以後,公有的方法就沒法獲取這些私有的變數了,所以你最好還是把它們都變成公有的。也可以把方法放在構造器中同時還能從外部獲取到它,這樣方法就能拿到構造器裡的私有變數了,不過這麼做的話又會出現一系列新的問題。

第二個方法中我們使用了Person構造器的”原型”(prototype)。一個函式的原型就是一個物件,當你需要在某個例項上解析它所呼叫到的欄位或者函式時你需要遍歷這個函式上所有的例項。所以這幾行程式碼所做的就是建立一個fullName方法的例項,然後所有的Person的例項都能直接呼叫到這方法,而不是對每個Person例項都新增一個fullName方法,造成方法的泛濫。我們也可以在構造器中用

this.fullName = function() { …

的方式定義fullName,但這樣每一個Person例項都會有fullName方法的副本,這不是我們希望的。

如果我們想要建立一個Person例項,我們可以這麼做:

var person = new Person("Justin", "Etheredge");
alert(person.fullName());

我們也可以建立一個繼承自Person的構造器:Spy構造器,我們會建立Spy的一個例項,不過只會宣告一個方法:

var Spy = function(firstName, lastName, age){
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
};
Spy.prototype = new Person();
 
Spy.prototype.spy = function(){
    alert(this.fullName() + " is spying.");   
}
 
var mySpy = new Spy("Mr.", "Spy", 50);
mySpy.spy();

正如你所看到的,我們建立了一個和Person很相似的構造器,但是它的原型是Person的一個例項。現在我們又新增上一些方法,使得Spy的例項又可以呼叫到Person的方法,同時還能直接取得Spy中的變數。這個方法比較複雜,不過一旦你明白怎麼使用了,你的程式碼就會變得很優雅。

結語

看到這裡,希望你已經學到了一些東西。不過這篇文章裡並沒有介紹多少關於“現代”JS的開發。這篇文章中涉及的還是舊知識,在過去幾年裡它們的使用面相當廣。希望你看完這篇文章以後,找到了學習JS的正確的方向。現在可能你把程式碼放到了不同的模組不同的檔案中(你應該做到這一點!),那麼下一步你要開始著手研究如何將JS結合和壓縮。如果你是使用Rails 3的開發者,可以在asset pipeline上免費獲取這些資訊或者工具。如果你是.NET開發者,你可以看看SquishIt框架,我就是從這裡開始的。如果你是ASP.NET MVC 4的開發者,也有相關的資源。

希望這篇文章對你有幫助。以後我也會介紹關於現代JS的開發,期待到時候能看到你的ID。

原文連結: codethinked 譯文連結: http://blog.jobbole.com/66135/

評論(1)

相關文章