作者: zyl910
一、緣由
在很多的面向對象編程語言中,我們可以使用命名空間(namespace)來組織代碼,避免全局變量污染、命名沖突。遺憾的是,JavaScript中並不提供對命名空間的原生支持。
有不少人提出各種辦法在JavaScript中模擬命名空間,但這些辦法存在以下問題——
- 辦法不統一。各種辦法各有優缺點,分別適合在不同的場合使用。但這也表示沒有統一辦法,有可能會造成代碼混亂。
- 部分辦法比較復雜,不易理解。有些得專門寫一些框架代碼,甚至有些得引用第三方的庫(如ExtJs等),甚至有些搞了復雜的模塊化方案。
- 不易定義類(class)。JavaScript有3種主流的定義類的辦法(構造函數、閉包、極簡主義),某些定義命名空間的辦法,會導致某種定義類的辦法失效。
- 瀏覽器兼容性問題。某些辦法用到了一些高版本的語法,導致只能用在某些瀏覽器中,而其他瀏覽器存在兼容性問題。
- 不利於自動生成文檔。因為某些辦法的代碼寫法比較復雜,無法被自動生成文檔的工具所識別。而缺少文檔的話,會導致龐大的代碼難以開發維護。
我查閱了大量資料,經過長期摸索,化繁為簡,終於找到了一種實現命名空間的最佳方案。該方案完美的解決了以上的5個問題,具有以下優點——
- 適用性廣。該辦法能幾乎能在任何場合下使用,使代碼風格統一。
- 定義簡單。使用簡單的JavaScript語句就能實現命名空間的定義。代碼量少,便於理解。
- 兼容主流的定義類的方法。即構造函數、閉包、極簡主義 這3種辦法定義的類,都能完美的放在命名空間里使用。
- 兼容所有的瀏覽器。因其采用了簡單的語法(貌似在ECMAScript 3.0的范圍內)。實測在 IE5~11、Edge、Chrome、Firefox中均測試通過。
- 支持用JSDuck生成文檔。且JSDuck能完美的識別命名空間,在文檔中展示。
該方案目前僅發現一個缺點——
- 必須寫帶命名空間的全名。即使是在同一個命名空間內,也是如此。畢竟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),並以樹形展示。且類、屬性、方法等的文檔也正確生成了。
五、心得總結
過去為了避免全局變量污染,一般是采用立即調用函數(IIFE)法寫閉包類,將私有數據封裝在一個類中。但該方案有2個缺點——
- 為了盡可能封裝、隱藏細節,可能會導致閉包內的代碼行數非常多,可讀性低,不易開發維護。
- 當代碼量大、使用多個js文件時,因為閉包不能跨文件,每個js文件都至少有一個閉包類的全局變量,即還是會在全局變量中占據多個名字。這時得小心命名,避免沖突。
而現在有了統一的命名空間方案后,便可放心的將復雜的閉包類,按照“低耦合高內聚”拆分為多個小的閉包類,並掛到命名空間中(給命名空間Object的字段賦值)。
而且,因隨時可以給命名空間Object增加新的字段。所以即使代碼分散在多個js文件中,也能使用同一個命名空間,測底避免全局變量污染。
源碼地址:
https://github.com/zyl910/test_jsduck
參考文獻
- ifcode《JS命名空間(namespace)》. http://www.jianshu.com/p/554454d951d9
- 默語《JavaScript之命名空間模式 淺析》. http://www.cnblogs.com/syfwhu/p/4885628.html
- 阮一峰《Javascript定義類(class)的三種方法》. http://www.ruanyifeng.com/blog/2012/07/three_ways_to_define_a_javascript_class.html
- 阮一峰《Javascript模塊化編程(一):模塊的寫法》. http://www.ruanyifeng.com/blog/2012/10/javascript_module.html
- JSDuck官網: https://github.com/senchalabs/jsduck
- zyl910《Javascript自動化文檔工具JSDuck在Windows下的使用心得》. http://www.cnblogs.com/zyl910/p/test_jsduck_on_windows.html