天天看點

JavaScript 匿名函數、子產品模式、閉包、命名空間、建立構造器(類)、繼承

今天無論是在浏覽器中還是在浏覽器外,JavaScript世界正在經曆翻天覆地地變化。如果我們談論腳本加載、用戶端的MVC架構、壓縮器、AMD、Common.js還有Coffeescript……隻會讓你的腦子發昏。對于那些已經早就熟知這些技術的人而言,或許很難想象到現在為止還有很多JS開發者還不熟悉這些工具,甚至事實上,他們很可能現在還不想去嘗試這些工具。

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

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

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

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

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

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

想要在匿名函數外部調用方法,我們可以将window對象傳入匿名函數,再将函數或變量值指派到這個對象上。為了保證這個window對象的引入不會造成什麼混亂,我們可以将widow對象作為一個變量傳入我們的匿名函數。當做函數傳入參數的方法同樣适用于第三方庫,甚至undefined這樣的值。現在我們的匿名函數看起來是這樣的:

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

任何我們想要從子產品中擷取的東西都可以通過window對象拿到。但是通常我不會直接将子產品中的内容直接複制到window對象上,而是會用更有組織性地将子產品中的内容。在大部分語言中,我們将這些容器稱為“命名空間”,在JS中我們可以用“對象”的方式來模拟。

如果我們想要聲明一個命名空間,将一個函數放進這個空間中,代碼可以寫成這樣:

我們隻是在全局環境中建立了一個用于檢視某個對象是否已經存在,如果已經存在了,那麼我們就可以直接使用;不然就需要用’{}’來建立一個新的對象。接着,我們可以開始添加這個命名空間的内容,将各種函數放入這個空間中,就像上面的代碼片段所做的那樣,但是我們又不希望這些函數就随便的放在那裡,而是希望将子產品和命名空間聯系在一起,就像下面這樣:

還可以這麼寫:

現在,我們不再是将window傳入我們的子產品中,我們将一個和window對象聯系在一起的命名空間傳入子產品中。之是以使用’||’的原因是我們可以重複使用同一個命名空間,而不是每次需要使用命名空間的時候我們又要重新建立一個。許多包含有命名空間方法的庫會幫你建立好空間的,或者你可以使用一些想namespace.js這樣的工具來建構嵌套的命名空間。由于在JS中,每一個在命名空間中的項你都不得不指定它的命名空間,是以通常我都盡量不會去建立深度嵌套的命名空間。如果你在MyApp.MyModule.MySubModule中建立了一個doSomething方法,你需要這麼引用它:

每次你要調用它,或者你可以在你的子產品中給這個命名空間一個别名:

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

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

一次就建立一個子產品,然後傳回一個包含有需要公有化的子產品片段的對象,同時子產品中需要保持私有的變量也不會被暴露。myModule變量會包含有兩個共有的項,不過其中Somefunction中的myVar2是從外部擷取不到的。

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

現在先看第一個函數,你會看到我們建立了一個Person構造器。我們用它來構造新的person對象。這個構造器需要3個傳入參數,然後将這3個參數指派到執行上下文中。我們也是通過這種方式擷取到公有執行個體變量。這裡也可以建立私有變量:将傳入參數指派到這個構造器中的局部變量。但是這麼做以後,公有的方法就沒法擷取這些私有的變量了,是以你最好還是把它們都變成公有的。也可以把方法放在構造器中同時還能從外部擷取到它,這樣方法就能拿到構造器裡的私有變量了,不過這麼做的話又會出現一系列新的問題。

第二個方法中我們使用了Person構造器的”原型”(prototype)。一個函數的原型就是一個對象,當你需要在某個執行個體上解析它所調用到的字段或者函數時你需要周遊這個函數上所有的執行個體。是以這幾行代碼所做的就是建立一個fullName方法的執行個體,然後所有的Person的執行個體都能直接調用到這方法,而不是對每個Person執行個體都添加一個fullName方法,造成方法的泛濫。我們也可以在構造器中用

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

如果我們想要建立一個Person執行個體,我們可以這麼做:

我們也可以建立一個繼承自Person的構造器:Spy構造器,我們會建立Spy的一個執行個體,不過隻會聲明一個方法:

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

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

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

繼續閱讀