JavaScript 實現命名空間(namespace)的最佳方案——兼容主流的定義類(class)的方法,兼容所有瀏覽器,支持用JSDuck生成文檔


作者: zyl910

一、緣由

在很多的面向對象編程語言中,我們可以使用命名空間(namespace)來組織代碼,避免全局變量污染、命名沖突。遺憾的是,JavaScript中並不提供對命名空間的原生支持。
有不少人提出各種辦法在JavaScript中模擬命名空間,但這些辦法存在以下問題——

  1. 辦法不統一。各種辦法各有優缺點,分別適合在不同的場合使用。但這也表示沒有統一辦法,有可能會造成代碼混亂。
  2. 部分辦法比較復雜,不易理解。有些得專門寫一些框架代碼,甚至有些得引用第三方的庫(如ExtJs等),甚至有些搞了復雜的模塊化方案。
  3. 不易定義類(class)。JavaScript有3種主流的定義類的辦法(構造函數、閉包、極簡主義),某些定義命名空間的辦法,會導致某種定義類的辦法失效。
  4. 瀏覽器兼容性問題。某些辦法用到了一些高版本的語法,導致只能用在某些瀏覽器中,而其他瀏覽器存在兼容性問題。
  5. 不利於自動生成文檔。因為某些辦法的代碼寫法比較復雜,無法被自動生成文檔的工具所識別。而缺少文檔的話,會導致龐大的代碼難以開發維護。

我查閱了大量資料,經過長期摸索,化繁為簡,終於找到了一種實現命名空間的最佳方案。該方案完美的解決了以上的5個問題,具有以下優點——

  1. 適用性廣。該辦法能幾乎能在任何場合下使用,使代碼風格統一。
  2. 定義簡單。使用簡單的JavaScript語句就能實現命名空間的定義。代碼量少,便於理解。
  3. 兼容主流的定義類的方法。即構造函數、閉包、極簡主義 這3種辦法定義的類,都能完美的放在命名空間里使用。
  4. 兼容所有的瀏覽器。因其采用了簡單的語法(貌似在ECMAScript 3.0的范圍內)。實測在 IE5~11、Edge、Chrome、Firefox中均測試通過。
  5. 支持用JSDuck生成文檔。且JSDuck能完美的識別命名空間,在文檔中展示。

該方案目前僅發現一個缺點——

  1. 必須寫帶命名空間的全名。即使是在同一個命名空間內,也是如此。畢竟JavaScript不是原生支持namespace的。該缺點只是稍微增加了一點代碼量,沒有其他負面影響。

二、辦法說明

其實這套辦法並不復雜,甚至很多文檔里其實講解過這種寫法。但是它們沒將這種寫法推廣到命名空間的通用寫法的高度,沒明確說這種辦法下如何支持各種定義類的寫法。我實驗了該辦法,發現它能適應各種情況,並具有適合用工具生成文檔等優點。

2.1 定義命名空間

2.1.1 定義頂層命名空間

若需定義一個名叫“jsnamespace”頂層命名空間,那么這樣寫——

var jsnamespace = window.jsnamespace || {};

其實就是使用對象字面量(object literal)的辦法聲明一個對象變量。即可理解為——

var jsnamespace = {};

賦值寫成 window.jsnamespace || {} ,是為了在重復定義時避免被誤覆蓋掉。這樣便能很方便的在多個文件里定義命名空間了。

2.1.1 定義子命名空間

若我們還要在“jsnamespace”里定義一個名叫“sub”子命名空間,即“jsnamespace.sub”,那么這樣寫——

jsnamespace.sub = window.jsnamespace.sub || {};

其實就是給 jsnamespace 對象變量加了一個 sub 字段,該字段也是一個對象變量。

可以采用此辦法,嵌套定義任意層次深的命名空間。

2.2 在命名空間中定義類

光有命名空間是沒什么用的,最關鍵是要能在里面存放各種類。

2.2.1 構造函數法的類

構造函數法的類,本質上是一個 Function 而已。所以即使將它放在對象變量(命名空間)內,只要能定位該Function,便能使用 new 創建對象。

若需在“jsnamespace”命名空間里定義一個名叫“PersonInfo”的構造函數法的類,那么這樣寫——

var jsnamespace = window.jsnamespace || {};

jsnamespace.PersonInfo = function(cfg) {
    cfg = cfg || {};
    this.name = cfg["name"] || "";
    this.gender = cfg["gender"] || "?";
};

可這樣使用該類——

    var p1 = new jsnamespace.PersonInfo();
    p1.name = "Zhang San";    // 張三.
    p1.gender = "男";

該用法與傳統的new類用法一致,僅是使用了帶命名控件的類名。
技術細節——對於JavaScript解析機制來說,它是從 jsnamespace 這個Object 的 PersonInfo 字段獲取到Function,然后再對該 Function 進行new操作創建對象。

2.2.2 閉包、極簡主義的類

對於 立即調用函數(IIFE)法返回的內容,它本質上是一個 Object 而已。只要按照JavaScript的規則,能合理的訪問到這些Object,那么就能使用 閉包法、極簡主義法定義的類了。

若需在“jsnamespace”命名空間里再定義一個名叫“PersonInfoUtil”的閉包法的類,那么這樣寫——

var jsnamespace = window.jsnamespace || {};

jsnamespace.PersonInfo = function(cfg) {
    cfg = cfg || {};
    this.name = cfg["name"] || "";
    this.gender = cfg["gender"] || "?";
};

jsnamespace.PersonInfoUtil = function () {
    return {
        show: function(p) {
            var s = "姓名:" + p.name;
            alert(s);
        }
    };
}();

可這樣使用該類——

    var p1 = new jsnamespace.PersonInfo();
    p1.name = "Zhang San";    // 張三.
    p1.gender = "男";
    jsnamespace.PersonInfoUtil.show(p1);

2.2.3 變量共享與各類之間調用

本命名空間辦法,不會干擾變量共享與各類之間調用。可以按照原來的辦法去處理。

簡單來說,本命名空間實際上就是 JavaScript 的Object。你使用“.”操作符,按照Object的特點找到所需的字段、函數,就能進行操作了。

三、完整范例

這里展示了完整的范例代碼,並加上了JSDuck風格的文檔注釋。

3.1 jsnamespace.js

jsnamespace 命名空間里有這些類——

  • GenderCode: 性別代碼. 枚舉類.
  • PersonInfo: 個人信息. 構造函數法的類.
  • PersonInfoUtil: 個人信息工具. 閉包法的類.
/** @class
* JavaScript的命名空間.
* @abstract
*/
var jsnamespace = window.jsnamespace || {};

// == enum ==

/** @enum
* 性別代碼. 枚舉類.
*/
jsnamespace.GenderCode = {
    /** 未知 */
    "UNKNOWN": 0,
    /** 男 */
    "MALE": 1,
    /** 女 */
    "FEMALE": 2
};


// == PersonInfo class ==

/** @class
* 個人信息. 構造函數法的類.
*/
jsnamespace.PersonInfo = function(cfg) {
    cfg = cfg || {};
    /** @cfg {String} [name=""] 姓名. */
    /** @property {String} 姓名. */
    this.name = cfg["name"] || "";
    /** @cfg {jsnamespace.GenderCode} [gender=jsnamespace.GenderCode.UNKNOWN] 性別. */
    /** @property {jsnamespace.GenderCode} 性別. */
    this.gender = cfg["gender"] || jsnamespace.GenderCode.UNKNOWN;
};

/**
* 取得稱謂.
*
* @return  {String}    返回稱謂字符串.
*/
jsnamespace.PersonInfo.prototype.getAppellation = function() {
    var rt = "";
    if (jsnamespace.GenderCode.MALE == this.gender) {
        rt = "Mr.";
    } else if (jsnamespace.GenderCode.FEMALE == this.gender) {
        rt = "Ms.";
    }
    return rt;
};

/**
* 取得歡迎字符串.
*
* @return  {String}    返回歡迎字符串.
*/
jsnamespace.PersonInfo.prototype.getHello = function() {
    var rt = "Hello, " + this.getAppellation() + " " + (this.name);
    return rt;
};


// == PersonInfoUtil class ==

/** @class
* 個人信息工具. 閉包法的類.
*/
jsnamespace.PersonInfoUtil = function () {
    /**
    * 前綴.
    *
    * @static @private
    */
    var _prefix = "[show] ";
    
    return {
        /** 顯示信息.
        *
        * @param {jsnamespace.PersonInfo}    p    個人信息.
        * @static
        */
        show: function(p) {
            var s = _prefix;
            if (!!p) {
                s += p.getHello();
            }
            alert(s);
        },
        
        /** 版本號. @readonly */
        version: 0x100
    };
}();

3.2 jsnamespace_sub.js

jsnamespace_sub.js演示了如何在多個文件中使用同一個頂層命名空間,並建立子命名空間。

jsnamespace.sub 命名空間里有這些類——

  • Animal: 動物. 極簡主義法的類.
  • Cat: 貓. 繼承自Animal. 極簡主義法的類.
// 聲明本模塊所依賴的命名空間.
var jsnamespace = window.jsnamespace || {};


/** @class
* 子命名空間.
* @abstract
*/
jsnamespace.sub = window.jsnamespace.sub || {};

// 極簡主義法(minimalist approach)定義類.

/**
* 動物.
*/
jsnamespace.sub.Animal = {
    /** 創建 動物.
    *
    * @return  {Animal}    返回所創建的對象.
    * @static
    */
    createNew: function(){
        var animal = {};
        /** 睡覺.
        */
        animal.sleep = function(){ alert("睡懶覺"); };
        return animal;
    }
};

/**
* 貓.
* @extends jsnamespace.sub.Animal
*/
jsnamespace.sub.Cat = {
    /** 聲音.
    * @static @protected
    */
    sound : "喵喵喵",
    /** 創建 貓.
    *
    * @return  {Cat}    返回所創建的對象.
    * @static
    */
    createNew: function(){
        var cat = jsnamespace.sub.Animal.createNew();
        /** 發聲.
        */
        cat.makeSound = function(){ alert(jsnamespace.sub.Cat.sound); };
        /** 修改聲音.
        * @param {String}    x    聲音.
        */
        cat.changeSound = function(x){ jsnamespace.sub.Cat.sound = x; };
        return cat;
    }
};

3.3 測試頁面

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>測試JavaScript 命名空間</title>
</head>
<body>
<script type="text/javascript" src="jsnamespace.js"></script>
<script type="text/javascript" src="jsnamespace_sub.js"></script>

<script type="text/javascript">

/** 測試. */
function doTest() {
    //alert(jsnamespace);
    var p1 = new jsnamespace.PersonInfo();
    p1.name = "Zhang San";    // 張三.
    p1.gender = jsnamespace.GenderCode.MALE;
    var p2 = new jsnamespace.PersonInfo({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE});    // 李四.
    jsnamespace.PersonInfoUtil.show(p1);
    jsnamespace.PersonInfoUtil.show(p2);
    //
    var c = jsnamespace.sub.Cat.createNew();
    c.makeSound();
}


</script>
<h1>測試JavaScript 命名空間</h1>

<input type="button" value="測試" OnClick="doTest();" title="doTest" />
<br/>
輸出:<br/>
<textarea id="txtresult" rows="12" style="width:95%"></textarea>

</body>
</html>

四、用JSDuck生成文檔

以下截圖,就是JSDuck根據上面的代碼所生成文檔。可發現它完美的識別了代碼中的命名空間(jsnamespace),並以樹形展示。且類、屬性、方法等的文檔也正確生成了。

img_namespace.png
img_namespace_sub.png

五、心得總結

過去為了避免全局變量污染,一般是采用立即調用函數(IIFE)法寫閉包類,將私有數據封裝在一個類中。但該方案有2個缺點——

  1. 為了盡可能封裝、隱藏細節,可能會導致閉包內的代碼行數非常多,可讀性低,不易開發維護。
  2. 當代碼量大、使用多個js文件時,因為閉包不能跨文件,每個js文件都至少有一個閉包類的全局變量,即還是會在全局變量中占據多個名字。這時得小心命名,避免沖突。

而現在有了統一的命名空間方案后,便可放心的將復雜的閉包類,按照“低耦合高內聚”拆分為多個小的閉包類,並掛到命名空間中(給命名空間Object的字段賦值)。

而且,因隨時可以給命名空間Object增加新的字段。所以即使代碼分散在多個js文件中,也能使用同一個命名空間,測底避免全局變量污染。

源碼地址:

https://github.com/zyl910/test_jsduck

參考文獻


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM