關(guān)于Javascript模塊化和命名空間管理的問題說明
先說說我們?yōu)槭裁匆K化吧。其實(shí)這還是和編碼思想和代碼管理的便利度相關(guān)(沒有提及名字空間污染的問題是因?yàn)槲蚁嘈乓呀?jīng)考慮到模塊化思想的編碼者應(yīng)該至少有了一套自己的命名法則,在中小型的站點(diǎn)中,名字空間污染的概率已經(jīng)很小了,但也不代表不存在,后面會(huì)說這個(gè)問題)。
其實(shí)模塊化思想還是和面向?qū)ο蟮乃枷肴绯鲆晦H,只不過可能我們口中所謂的“模塊”是比所謂的“對(duì)象”更大的對(duì)象而已。我們把致力完成同一個(gè)目的的功能函數(shù)通過良好的封裝組合起來,并且保證其良好的復(fù)用性,我們大概可以把這樣一個(gè)組合代碼片段的思想稱為面向?qū)ο蟮乃枷?。這樣做的好處有很多,比如:易用性,通用性,可維護(hù)性,可閱讀性,規(guī)避變量名污染等等。
而模塊化無非就是在面向?qū)ο笊系拿嫦蚰K而已,我們把和同一個(gè)項(xiàng)目(模塊)相關(guān)的功能封裝有機(jī)的組合起來,通過一個(gè)共同的名字來管理。就大概可以說是模塊化的思想。所以,相比面向?qū)ο蠖缘脑?,我覺得在代碼架構(gòu)上貫徹模塊化的思想其實(shí)比面向?qū)ο蟮呢瀼剡€更為容易一些。
不像c#,java等這種本身就擁有良好模塊化和命名空間機(jī)制的強(qiáng)類型語言。JavaScript并沒有為創(chuàng)建和管理模塊而提供任何語言功能。正因?yàn)檫@樣,我們?cè)谧鰆s的編碼的某些時(shí)候,對(duì)于所謂的命名空間(namespace)的使用會(huì)顯得有些過于隨便(包括我自己)。比如 :
var Hongru = {} // namespace
(function(){
Hongru.Class1 = function () {
//TODO
}
...
Hongru.Class2 = function () {
//TODO
}
})();
如上,我們通常用一個(gè)全局變量或者全局對(duì)象就作為我們的namespace,如此簡(jiǎn)單,甚至顯得有些隨便的委以它這么重大的責(zé)任。但是我們能說這樣做不好嗎?不能,反而是覺得能有這種編碼習(xí)慣的同學(xué)應(yīng)該都值得表揚(yáng)。。。
所以,我們?cè)谧鲆恍╉?xiàng)目的時(shí)候或者建一些規(guī)模不大的網(wǎng)站時(shí),簡(jiǎn)單的用這種方式來做namespace的工作其實(shí)也夠了,基本不會(huì)出什么大亂子。但是回歸本質(zhì),如果是有代碼潔癖或者是建立一個(gè)大規(guī)模的網(wǎng)站,抑或一開始就抱著絕對(duì)優(yōu)雅的態(tài)度和邏輯來做代碼架構(gòu)的話?;蛟S我們?cè)摽紤]更好一些的namespace的注冊(cè)和管理方式。
在這個(gè)方面,jQuery相比于YUI,Mootool,EXT等,就顯得稍遜一籌,(雖然jq也有自己的一套模塊化機(jī)制),但這依然不妨礙我們對(duì)它的喜愛,畢竟側(cè)重點(diǎn)不同,jq強(qiáng)是強(qiáng)在它的選擇器,否則他也不會(huì)叫j-Query了。
所以我們說jQuery比較適合中小型的網(wǎng)站也不無道理。就像豆瓣的開源的前端輕量級(jí)框架Do框架一樣,也是建立在jQuery上,封裝了一層模塊化管理的思想和文件同步載入的功能。
【關(guān)于namespace】
好了,我們回歸正題,如上的方式,簡(jiǎn)單的通過全局對(duì)象來做namespace已經(jīng)能夠很好的減少全局變量,規(guī)避變量名污染的問題,但是一旦網(wǎng)站規(guī)模變大,或者項(xiàng)目很多的時(shí)候,管理多個(gè)全局對(duì)象的名字空間依然會(huì)有問題。如果不巧發(fā)生了名字沖突,一個(gè)模塊就會(huì)覆蓋掉另一個(gè)模塊的屬性,導(dǎo)致其一或者兩者都不能正常工作。而且出現(xiàn)問題之后,要去甄別也挺麻煩。所以我們可能需要一套機(jī)制或者工具,能在創(chuàng)建namespace的時(shí)候就能判斷是否已有重名。
另一方面,不同模塊,亦即不同namespace之間其實(shí)也不能完全獨(dú)立,有時(shí)候我們也需要在不同名字空間下建立相同的方法或?qū)傩?,這時(shí)方法或?qū)傩缘膶?dǎo)入和導(dǎo)出也會(huì)是個(gè)問題。
就以上兩個(gè)方面,我稍微想了想,做了些測(cè)試,但依然有些紕漏。今天又重新翻了一下“犀牛書”,不愧是經(jīng)典,上面的問題,它輕而易舉就解決了?;凇跋钡慕鉀Q方案和demo,我稍微做了些修改和簡(jiǎn)化。把自己的理解大概分享出來。比較重要的有下面幾個(gè)點(diǎn):
--測(cè)試每一個(gè)子模塊的可用性
由于我們的名字空間是一個(gè)對(duì)象,擁有對(duì)象應(yīng)該有的層級(jí)關(guān)系,所以在檢測(cè)名字空間的可用性時(shí),需要基于這樣的層級(jí)關(guān)系去判斷和注冊(cè),這在注冊(cè)一個(gè)子名字空間(sub-namespace)時(shí)尤為重要。比如我們新注冊(cè)了一個(gè)名字空間為Hongru,然后我們需要再注冊(cè)一個(gè)名字空間為Hongru.me,亦即我們的本意就是me這個(gè)namespace是Hongru的sub-namespace,他們應(yīng)該擁有父子的關(guān)系。所以,在注冊(cè)namespace的時(shí)候需要通過‘.'來split,并且進(jìn)行逐一對(duì)應(yīng)的判斷。所以,注冊(cè)一個(gè)名字空間的代碼大概如下:
// create namespace --> return a top namespace
Module.createNamespace = function (name, version) {
if (!name) throw new Error('name required');
if (name.charAt(0) == '.' || name.charAt(name.length-1) == '.' || name.indexOf('..') != -1) throw new Error('illegal name');
var parts = name.split('.');
var container = Module.globalNamespace;
for (var i=0; i<parts.length; i++) {
var part = parts[i];
if (!container[part]) container[part] = {};
container = container[part];
}
var namespace = container;
if (namespace.NAME) throw new Error('module "'+name+'" is already defined');
namespace.NAME = name;
if (version) namespace.VERSION = version;
Module.modules[name] = namespace;
return namespace;
};
注:上面的Module是我們來注冊(cè)和管理namespace的一個(gè)通用Module,它本身作為一個(gè)“基模塊”,擁有一個(gè)modules的模塊隊(duì)列屬性,用來存儲(chǔ)我們新注冊(cè)的名字空間,正因?yàn)橛辛诉@個(gè)隊(duì)列,我們才能方便的判斷namespace時(shí)候已經(jīng)被注冊(cè):
var Module;
//check Module --> make sure 'Module' is not existed
if (!!Module && (typeof Module != 'object' || Module.NAME)) throw new Error("NameSpace 'Module' already Exists!");
Module = {};
Module.NAME = 'Module';
Module.VERSION = 0.1;
Module.EXPORT = ['require',
'importSymbols'];
Module.EXPORT_OK = ['createNamespace',
'isDefined',
'modules',
'globalNamespace'];
Module.globalNamespace = this;
Module.modules = {'Module': Module};
上面代碼最后一行就是一個(gè)namespace隊(duì)列,所有新建的namespace都會(huì)放到里面去。結(jié)合先前的一段代碼,基本就能很好的管理我們的名字空間了,至于Module這個(gè)“基模塊”還有些EXPORT等別的屬性,等會(huì)會(huì)接著說。下面是一個(gè)創(chuàng)建名字空間的測(cè)試demo
Module.createNamespace('Hongru', 0.1);//注冊(cè)一個(gè)名為Hongru的namespace,版本為0.1
上面第二個(gè)版本參數(shù)也可以不用,如果你不需要版本號(hào)的話。在chrome-debugger下可以看到我們新注冊(cè)的名字空間
可以看到新注冊(cè)的Hongru命名空間已經(jīng)生效:再看看Module的模塊隊(duì)列:
可以發(fā)現(xiàn),新注冊(cè)的Hongru也添進(jìn)了Module的modules隊(duì)列里。大家也發(fā)現(xiàn)了,Module里還有require,isDefined,importSymbols幾個(gè)方法。
由于require(檢測(cè)版本),isDefined(檢測(cè)namespace時(shí)候已經(jīng)注冊(cè))這兩個(gè)方法并不難,就稍微簡(jiǎn)略點(diǎn):
--版本和重名檢測(cè)
// check name is defined or not
Module.isDefined = function (name) {
return name in Module.modules;
};
// check version
Module.require = function (name, version) {
if (!(name in Module.modules)) throw new Error('Module '+name+' is not defined');
if (!version) return;
var n = Module.modules[name];
if (!n.VERSION || n.VERSION < version) throw new Error('version '+version+' or greater is required');
};
上面兩個(gè)方法都很簡(jiǎn)單,相信大家都明白,一個(gè)是隊(duì)列檢測(cè)是否重名,一個(gè)檢測(cè)版本是否達(dá)到所需的版本。也沒有什么特別的地方,就不細(xì)講了,稍微復(fù)雜一點(diǎn)的是名字空間之間的屬性或方法的相互導(dǎo)入的問題。
--名字空間中標(biāo)記的屬性或方法的導(dǎo)出
由于我們要的是一個(gè)通用的名字空間注冊(cè)和管理的tool,所以在做標(biāo)記導(dǎo)入或?qū)С龅臅r(shí)候需要考慮到可配置性,不能一股腦全部導(dǎo)入或?qū)С觥K跃陀辛宋覀兛吹降腗odule模板中的EXPORT和EXPORT_OK兩個(gè)Array作為存貯我們?cè)试S導(dǎo)出的屬性或方法的標(biāo)記隊(duì)列。其中EXPORT為public的標(biāo)記隊(duì)列,EXPORT_OK為我們可以自定義的標(biāo)記隊(duì)列,如果你覺得不要分這么清楚,也可以只用一個(gè)標(biāo)記隊(duì)列,用來存放你允許導(dǎo)出的標(biāo)記屬性或方法。
有了標(biāo)記隊(duì)列,我們做的導(dǎo)出操作就只針對(duì)EXPORT和EXPORT_OK兩個(gè)標(biāo)記隊(duì)列中的標(biāo)記。
// import module
Module.importSymbols = function (from) {
if (typeof form == 'string') from = Module.modules[from];
var to = Module.globalNamespace; //dafault
var symbols = [];
var firstsymbol = 1;
if (arguments.length>1 && typeof arguments[1] == 'object' && arguments[1] != null) {
to = arguments[1];
firstsymbol = 2;
}
for (var a=firstsymbol; a<arguments.length; a++) {
symbols.push(arguments[a]);
}
if (symbols.length == 0) {
//default export list
if (from.EXPORT) {
for (var i=0; i<from.EXPORT.length; i++) {
var s = from.EXPORT[i];
to[s] = from[s];
}
return;
} else if (!from.EXPORT_OK) {
// EXPORT array && EXPORT_OK array both undefined
for (var s in from) {
to[s] = from[s];
return;
}
}
}
if (symbols.length > 0) {
var allowed;
if (from.EXPORT || form.EXPORT_OK) {
allowed = {};
if (from.EXPORT) {
for (var i=0; i<form.EXPORT.length; i++) {
allowed[from.EXPORT[i]] = true;
}
}
if (from.EXPORT_OK) {
for (var i=0; i<form.EXPORT_OK.length; i++) {
allowed[form.EXPORT_OK[i]] = true;
}
}
}
}
//import the symbols
for (var i=0; i<symbols.length; i++) {
var s = symbols[i];
if (!(s in from)) throw new Error('symbol '+s+' is not defined');
if (!!allowed && !(s in allowed)) throw new Error(s+' is not public, cannot be imported');
to[s] = form[s];
}
}
這個(gè)方法中第一個(gè)參數(shù)為導(dǎo)出源空間,第二個(gè)參數(shù)為導(dǎo)入目的空間(可選,默認(rèn)是定義的globalNamespace),后面的參數(shù)也是可選,為你想導(dǎo)出的具體屬性或方法,默認(rèn)是標(biāo)記隊(duì)列里的全部。
下面是測(cè)試demo:
Module.createNamespace('Hongru');
Module.createNamespace('me', 0.1);
me.EXPORT = ['define']
me.define = function () {
this.NAME = '__me';
}
Module.importSymbols(me, Hongru);//把me名字空間下的標(biāo)記導(dǎo)入到Hongru名字空間下
可以看到測(cè)試結(jié)果:

本來定義在me名字空間下的方法define()就被導(dǎo)入到Hongru名字空間下了。當(dāng)然,這里說的導(dǎo)入導(dǎo)出,其實(shí)只是copy,在me名字空間下依然能訪問和使用define()方法。
好了,大概就說到這兒吧,這個(gè)demo也只是提供一種管理名字空間的思路,肯定有更加完善的方法,可以參考下YUI,EXT等框架。或者參考《JavaScript權(quán)威指南》中模塊和名字空間那節(jié)。
最后貼下源碼:
/* == Module and NameSpace tool-func ==
* author : hongru.chen
* date : 2010-12-05
*/
var Module;
//check Module --> make sure 'Module' is not existed
if (!!Module && (typeof Module != 'object' || Module.NAME)) throw new Error("NameSpace 'Module' already Exists!");
Module = {};
Module.NAME = 'Module';
Module.VERSION = 0.1;
Module.EXPORT = ['require',
'importSymbols'];
Module.EXPORT_OK = ['createNamespace',
'isDefined',
'modules',
'globalNamespace'];
Module.globalNamespace = this;
Module.modules = {'Module': Module};
// create namespace --> return a top namespace
Module.createNamespace = function (name, version) {
if (!name) throw new Error('name required');
if (name.charAt(0) == '.' || name.charAt(name.length-1) == '.' || name.indexOf('..') != -1) throw new Error('illegal name');
var parts = name.split('.');
var container = Module.globalNamespace;
for (var i=0; i<parts.length; i++) {
var part = parts[i];
if (!container[part]) container[part] = {};
container = container[part];
}
var namespace = container;
if (namespace.NAME) throw new Error('module "'+name+'" is already defined');
namespace.NAME = name;
if (version) namespace.VERSION = version;
Module.modules[name] = namespace;
return namespace;
};
// check name is defined or not
Module.isDefined = function (name) {
return name in Module.modules;
};
// check version
Module.require = function (name, version) {
if (!(name in Module.modules)) throw new Error('Module '+name+' is not defined');
if (!version) return;
var n = Module.modules[name];
if (!n.VERSION || n.VERSION < version) throw new Error('version '+version+' or greater is required');
};
// import module
Module.importSymbols = function (from) {
if (typeof form == 'string') from = Module.modules[from];
var to = Module.globalNamespace; //dafault
var symbols = [];
var firstsymbol = 1;
if (arguments.length>1 && typeof arguments[1] == 'object' && arguments[1] != null) {
to = arguments[1];
firstsymbol = 2;
}
for (var a=firstsymbol; a<arguments.length; a++) {
symbols.push(arguments[a]);
}
if (symbols.length == 0) {
//default export list
if (from.EXPORT) {
for (var i=0; i<from.EXPORT.length; i++) {
var s = from.EXPORT[i];
to[s] = from[s];
}
return;
} else if (!from.EXPORT_OK) {
// EXPORT array && EXPORT_OK array both undefined
for (var s in from) {
to[s] = from[s];
return;
}
}
}
if (symbols.length > 0) {
var allowed;
if (from.EXPORT || form.EXPORT_OK) {
allowed = {};
if (from.EXPORT) {
for (var i=0; i<form.EXPORT.length; i++) {
allowed[from.EXPORT[i]] = true;
}
}
if (from.EXPORT_OK) {
for (var i=0; i<form.EXPORT_OK.length; i++) {
allowed[form.EXPORT_OK[i]] = true;
}
}
}
}
//import the symbols
for (var i=0; i<symbols.length; i++) {
var s = symbols[i];
if (!(s in from)) throw new Error('symbol '+s+' is not defined');
if (!!allowed && !(s in allowed)) throw new Error(s+' is not public, cannot be imported');
to[s] = form[s];
}
}
相關(guān)文章
JS實(shí)現(xiàn)選中當(dāng)前菜單后高亮顯示的導(dǎo)航條效果
這篇文章主要介紹了JS實(shí)現(xiàn)選中當(dāng)前菜單后高亮顯示的導(dǎo)航條效果,涉及JavaScript針對(duì)頁面元素的遍歷及樣式動(dòng)態(tài)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10Openlayers3實(shí)現(xiàn)車輛軌跡回放功能
這篇文章主要介紹了Openlayers3實(shí)現(xiàn)車輛軌跡回放功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09JavaScript實(shí)現(xiàn)body內(nèi)任意節(jié)點(diǎn)的自定義屬性功能示例
這篇文章主要介紹了JavaScript實(shí)現(xiàn)body內(nèi)任意節(jié)點(diǎn)的自定義屬性功能,涉及javascript針對(duì)DOM節(jié)點(diǎn)的獲取及屬性設(shè)置相關(guān)操作技巧,需要的朋友可以參考下2017-09-09JavaScript中call,apply,bind的區(qū)別與實(shí)現(xiàn)
這篇文章主要介紹了JavaScript中call,apply,bind的區(qū)別與實(shí)現(xiàn),文章通過圍繞主題思想展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09JavaScript canvas實(shí)現(xiàn)跟隨鼠標(biāo)移動(dòng)小球
這篇文章主要為大家詳細(xì)介紹了JavaScript canvas實(shí)現(xiàn)跟隨鼠標(biāo)移動(dòng)小球,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-02-02純js實(shí)現(xiàn)瀑布流展現(xiàn)照片(自動(dòng)適應(yīng)窗口大小)
用瀑布流來展現(xiàn)照片再好不過了,我的思路大概是一張一張的圖片插入,當(dāng)這一行的圖片保持長(zhǎng)寬比例不變并且高度低于250時(shí)就完成一個(gè)了循環(huán),即這一行插入進(jìn)去了2013-04-04使用json-server簡(jiǎn)單完成CRUD模擬后臺(tái)數(shù)據(jù)的方法
這篇文章主要介紹了使用json-server簡(jiǎn)單完成CRUD模擬后臺(tái)數(shù)據(jù)的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-07-07JS簡(jiǎn)單的輪播的圖片滾動(dòng)實(shí)例
JS簡(jiǎn)單的輪播的圖片滾動(dòng)實(shí)例,需要的朋友可以參考一下2013-06-06XMLHttpRequest對(duì)象_Ajax異步請(qǐng)求重點(diǎn)(推薦)
下面小編就為大家?guī)硪黄猉MLHttpRequest對(duì)象_Ajax異步請(qǐng)求重點(diǎn)(推薦)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09js獲取時(shí)間函數(shù)及擴(kuò)展函數(shù)的方法
下面小編就為大家?guī)硪黄猨s獲取時(shí)間函數(shù)及擴(kuò)展函數(shù)的方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-10-10