原文:https://www.cnblogs.com/zyl910/p/js_namespace_bestpractice.html
一、缘由
在很多的面向对象编程语言中,我们可以使用命名空间(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