【譯】JavaScript 命名空間

ChandraPret 8年前發布 | 25K 次閱讀 Perl JavaScript開發 JavaScript

JavaScript 中有很多可以給你的對象安全分配命名空間的方法。這篇文章討論我見過的普遍的實踐。

前綴命名空間

如果命名空間的目的是避免沖突的話。下面這個系統,只要我們知道全局變量名前綴 myApp_ 是唯一的,可以像其他系統一樣避免命名空間沖突。

// add uniquely named global properties
var myApp_sayHello = function() {
  alert('hello');
};
var myApp_sayGoodbye = function() {
  alert('goodbye');
};

// use the namespace properties
myApp_sayHello();

C 語言程序經常使用前綴命名空間。在 JavaScript 的世界中,你可能會碰見 Macromedia 的 MM_ 方法,例如 MM_showHideLayers。

我認為前綴命名空間是 JavaScript 中最清楚明白的命名空間系統。(下面的對象命名空間策略在加入了 this 關鍵字后會導致困惑。)

前綴命名空間的確創建了很多全局對象。這對于前綴用來避免的命名空間沖突并不是什么問題。前綴命名空間的問題是,有些網頁瀏覽器(例如 IE6)在有很多全局對象時表現很糟糕,就我所聽說。我做了一些測試并且發現有一個 comp.lang.javascript 的小線程,不過我沒有就這個話題研究徹底。

單對象命名空間

當下,最流行的 JavaScript 命名空間實踐是使用一個全局變量來引用一個對象。這個被引用的對象引用你的『真正的業務』,并且因為你的全局對象的命名獨一無二,你的代碼和其他人的代碼就可以一起嗨皮地運行。

如果你確定這個世界上沒有任何人用了這個全局變量名 myApp ,那么你可以有這樣的代碼:

// define the namespace object
var myApp = {};

// add properties to the namespace object
myApp.sayHello = function() {
  alert('hello');
};
myApp.sayGoodbye = function() {
  alert('goodbye');
};

// use the namespace properties
myApp.sayHello();

當上面代碼的最后一行執行時,JavaScript 解釋器首先找到 myApp 對象,然后找到并調用這個對象的 syaHello 屬性。

對象命名空間的一個問題是它會導致與面向對象消息傳遞混淆。這兩者之間并沒有明顯的句法差異:

// 1
namespace.prop();

// 2
receiver.message();

更仔細地研究這個混淆,我們得出下面的命名空間想法。假設我們有以下庫。

var myApp = {};

myApp.message = 'hello';

myApp.sayHello = function() {
  alert(myApp.message);
};

用這個庫的代碼可以隨意進行寫操作。

myApp.sayHello(); // works

var importedfn = myApp.sayHello;

importedfn(); // works

將這個和那個令人混淆的使用 this 的消息傳遞版本比較一下。

var myApp = {};

myApp.message = 'hello';

myApp.sayHello = function() {
  alert(this.message);
};

用這個庫的代碼可以隨意進行寫操作。

myApp.sayHello() // works because "this" refers to myApp object.

var importedfn = myApp.sayHello;

importedfn(); // error because "this" refers to global object.

這里面的要上的一課是, this 永遠不能引用一個被作為命名空間的對象因為它肯能導致關于從命名空間引入標識符的混淆。這個問題是 this 在我的 JavaScript Warning Words 列表中的原因之一。

(這也表明了庫的 API 屬性應該指向用一個方法,這樣這些方法可以被導入其他命名空間。這個問題是在我的文章 Lazy Function Definition Pattern 的評論中被指出的。懶惰方法定義可以在被隱藏在庫中并且不是 API 的部分時安全使用。)

嵌套對象命名空間

嵌套對象命名空間是另一個普遍的實踐,它擴展了對象命名空間的想法。你可能見過類似如下代碼:

YAHOO.util.Event.addListener(/*...*/)

解決上面的代碼需要解釋器首先找到全聚德 YAHOO 對象,然后它的 util 對象,然后它的 Event 對象,然后找到并調用它的 addListener 屬性。這樣的話每次事件處理器綁定到一個 DOM 元素上花的功夫太多了,因此導入的概念開始被采用。

(function() {
  var yue = YAHOO.util.Event;
  yue.addListener(/*...*/);
  yue.addListener(/*...*/);
})();

如果你清楚 YAHOO.util.Event.addListener 方法不會用 this 關鍵字并且永遠引用同一個方法,那么導入可以變得更加簡潔。

(function() {
  var yuea = YAHOO.util.Event.addEventListener;
  yuea(/*...*/);
  yuea(/*...*/);
})();

我覺得當目的只是避免標識符沖突時,嵌套對象命名空間的復雜是不必要的。難道 Yahoo! 還覺得這些全局標識符 YAHOO_util_Event 和 YAHOO_util_Event_addEventListener 不夠獨特嗎?

我認為使用嵌套對象命名空間的動機是要看起來和 Java 包命名傳統一樣,這在 Java 中開銷不大。例如,在 Java 中你可能看到如下:

package mytools.text;

class TextComponent {
  /* ... */
}

一個這個類的完全合格的引用應該是 mytools.text.TextComponent 。

下面是 Niemeyer 和 Knudsen (寫)的 Learning Java 中包命名的描述:

包名是按層級構成的,使用點分隔的命名傳統。包名組成成分給編譯器和運行系統構成了獨一無二的定位文件的路徑。然而,它們并沒在包之間創建其他的關系。并沒有什么『subpackage』的說法,事實上,包命名空間是直接的,而非層級的。在包層級關系特定部分的包僅僅是因為習慣而有關聯。比如,如果我們穿件了另一個叫做 mytools.text.poetry 的包(假設是為了跟詩有關的一些文字類),這些類并不是 mytools.text 包的一部分;它們沒有包成員的訪問權限。

嵌套命名空間的幻覺在 Perl 中也存在。在 Perl 中,嵌套包名由雙冒號分隔開。你可以看到如下 Perl 代碼:

package Red::Blue;
our $var = 'foo';

一個完全合格的上述變量引用應該是 $Red::Blue::var

在 Perl 中,就像 Java,命名空間層級的主意只是方便程序員,而不是語言本身要求。Wall,Christiansen 和 Orwant 的 Programming Perl 解釋道:

雙冒號可被用于鏈接在包名 $Red::Blue::var 中標識符。這意味著 $var 屬于包 Red::Blue 。包 Red::Blue 跟可能存在的 Red 包或 Blue 包一點關系都沒有。只是說, Red::BlueRed 或者 Blue 之間的關系可能對于寫代碼或者使用這個程序的人有什么意義,但跟 Perl 沒關系。(好吧,除了在現在的實現中,符號表 Red::Blue 剛好存在符號表 Red 中。但是 Perl 語言并沒有直接利用過它。)

上述引用中最后備注暗示了 Perl 可能有和在 JavaScript 中使用嵌套命名空間對象一樣的標識符沖突開銷。如果 Perl 的實現改變了,這個開銷就會消失。在 JavaScript 中,我肯定嵌套對象命名空間的開銷永遠不會消失因為 JavaScript 使用延遲綁定。

我并不認為 JavaScript 中的嵌套對象命名空間提供了任何大好處,不過如果不使用導入的話在運行時可能會開銷非常大。

一個折中方案

如果單純地前綴命名空間在某些瀏覽器中真的很慢,而嵌套命名空間的概念幫助在開發者腦中保持各事務的有序,那我認為上述 Yahoo! 的例子也可以這樣寫:

YAHOO.util_Event_addListener

或者用更多的全局名稱:

YAHOO_util_Event.addListener

哪個維度的命名空間?

Perl 的 CPAN 模塊是基于他們所做的事情進行命名空間管理的。例如,我寫了一個這個命名空間里的模塊:

JavaScript::Minifier

如果別人用同樣的名字寫他自己的模塊,并且他不自知地通過某些模塊依賴通過同一個名字使用 CPAN 模塊,那么就會有沖突。

Java 程序員采用最冗長但當然也是最安全的方法。(Java 程序員似乎都想著在大型系統上運行的代碼。)在 Java 中,包經常是基于 誰寫的做什么的 來命名。( myFunc 風格的規范化。)『誰寫的』部分甚至使用開發者自己的相對可以保證唯一性的名字。如果我寫一個 Java 的 minifier,因為我有 michaux.ca 的域名,我可能用以下命名空間:

ca.michaux.javascript.minifier

在 JavaScript 中,經過這次討論,可能這樣寫效率更高:

ca_michaux_javascript_minifier

因為 JavaScript 是以文本的形式服務的,這樣的命名空間可能開銷太大,因為增加了下載時間。Gzip 壓縮會找到公共的字符串并用短字符串替換它們。如果 gzip 不可用的話那么就可以考慮使用導入了。

var ca_michaux_javascript_minifier = {};

(function() {
  var cmjm = ca_michaux_javascript_minifier;

  // refer to cmjm inside this module pattern
})();

我并不是說這些長的命名空間是絕對必須的,不過他們一定是避免命名空間沖突的最安全方法。

其他命名空間問題

標識符不僅在 JavaScript 資源中創建。一個表單的 name 屬性也被加在 document.forms 對象上。像

這樣命名是有意義的。

命名空間類名屬性,比如 <div class=”myCompany_story”> ,可以在減少 CSS 命名空間沖突以及當 JavaScript 代碼在通過類名搜索 DOM 元素時很有價值。

總結

我個人認為,任何像 YAHOO.util.Event.addListener 這樣有點或者下劃線的東西都是被沖突嚇傻了的。它可以就是 YUI.on 。Dojo 通過同樣功能的 dojo.connect 提供了足夠的保護,因為它有效地涵蓋了命名空間『誰』和『做什么』的維度。沒有人會在他們的右腦中會這樣想并在 dojo 命名空間下寫一個 JavaScript 庫。Dojo 的開發人員也不會忘記他們已經有了一個 connect 方法并寫另外一個。

如果我們能有一個網站來讓程序員們保存他們的 JavaScript 全局標識符和下劃線前綴,并且當 ECMAScipt4 發布了的時候也包括他們的包名,就好了:『JavaScript 命名空間登記處』。

JavaScript 是一個最小概念集的強大語言。即使 JavaScript 并沒有專門為避免命名空間沖突設計的語言級支持,還是有很多解決問題的方法。并沒有一個『正確』的答案。選一個你最喜歡的。

不過,無論你做什么,請記住別弄一個另外的全局 $ 標識符。

 

來自: http://chengkang.me/2016/06/28/javascript-namespace-by-michaux/

 

 本文由用戶 ChandraPret 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!