作者: 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”顶层命名空间,那么这样写——

  1. var jsnamespace = window.jsnamespace || {};

其实就是使用对象字面量(object literal)的办法声明一个对象变量。即可理解为——

  1. var jsnamespace = {};

赋值写成 window.jsnamespace || {} ,是为了在重复定义时避免被误覆盖掉。这样便能很方便的在多个文件里定义命名空间了。

2.1.1 定义子命名空间

若我们还要在“jsnamespace”里定义一个名叫“sub”子命名空间,即“jsnamespace.sub”,那么这样写——

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

其实就是给 jsnamespace 对象变量加了一个 sub 字段,该字段也是一个对象变量。

可以采用此办法,嵌套定义任意层次深的命名空间。

2.2 在命名空间中定义类

光有命名空间是没什么用的,最关键是要能在里面存放各种类。

2.2.1 构造函数法的类

构造函数法的类,本质上是一个 Function 而已。所以即使将它放在对象变量(命名空间)内,只要能定位该Function,便能使用 new 创建对象。

若需在“jsnamespace”命名空间里定义一个名叫“PersonInfo”的构造函数法的类,那么这样写——

  1. var jsnamespace = window.jsnamespace || {};
  2. jsnamespace.PersonInfo = function(cfg) {
  3. cfg = cfg || {};
  4. this.name = cfg["name"] || "";
  5. this.gender = cfg["gender"] || "?";
  6. };

可这样使用该类——

  1. var p1 = new jsnamespace.PersonInfo();
  2. p1.name = "Zhang San"; // 张三.
  3. p1.gender = "男";

该用法与传统的new类用法一致,仅是使用了带命名控件的类名。

技术细节——对于JavaScript解析机制来说,它是从 jsnamespace 这个Object 的 PersonInfo 字段获取到Function,然后再对该 Function 进行new操作创建对象。

2.2.2 闭包、极简主义的类

对于 立即调用函数(IIFE)法返回的内容,它本质上是一个 Object 而已。只要按照JavaScript的规则,能合理的访问到这些Object,那么就能使用 闭包法、极简主义法定义的类了。

若需在“jsnamespace”命名空间里再定义一个名叫“PersonInfoUtil”的闭包法的类,那么这样写——

  1. var jsnamespace = window.jsnamespace || {};
  2. jsnamespace.PersonInfo = function(cfg) {
  3. cfg = cfg || {};
  4. this.name = cfg["name"] || "";
  5. this.gender = cfg["gender"] || "?";
  6. };
  7. jsnamespace.PersonInfoUtil = function () {
  8. return {
  9. show: function(p) {
  10. var s = "姓名:" + p.name;
  11. alert(s);
  12. }
  13. };
  14. }();

可这样使用该类——

  1. var p1 = new jsnamespace.PersonInfo();
  2. p1.name = "Zhang San"; // 张三.
  3. p1.gender = "男";
  4. jsnamespace.PersonInfoUtil.show(p1);

2.2.3 变量共享与各类之间调用

本命名空间办法,不会干扰变量共享与各类之间调用。可以按照原来的办法去处理。

简单来说,本命名空间实际上就是 JavaScript 的Object。你使用“.”操作符,按照Object的特点找到所需的字段、函数,就能进行操作了。

三、完整范例

这里展示了完整的范例代码,并加上了JSDuck风格的文档注释。

3.1 jsnamespace.js

jsnamespace 命名空间里有这些类——

  • GenderCode: 性别代码. 枚举类.
  • PersonInfo: 个人信息. 构造函数法的类.
  • PersonInfoUtil: 个人信息工具. 闭包法的类.
  1. /** @class
  2. * JavaScript的命名空间.
  3. * @abstract
  4. */
  5. var jsnamespace = window.jsnamespace || {};
  6. // == enum ==
  7. /** @enum
  8. * 性别代码. 枚举类.
  9. */
  10. jsnamespace.GenderCode = {
  11. /** 未知 */
  12. "UNKNOWN": 0,
  13. /** 男 */
  14. "MALE": 1,
  15. /** 女 */
  16. "FEMALE": 2
  17. };
  18. // == PersonInfo class ==
  19. /** @class
  20. * 个人信息. 构造函数法的类.
  21. */
  22. jsnamespace.PersonInfo = function(cfg) {
  23. cfg = cfg || {};
  24. /** @cfg {String} [name=""] 姓名. */
  25. /** @property {String} 姓名. */
  26. this.name = cfg["name"] || "";
  27. /** @cfg {jsnamespace.GenderCode} [gender=jsnamespace.GenderCode.UNKNOWN] 性别. */
  28. /** @property {jsnamespace.GenderCode} 性别. */
  29. this.gender = cfg["gender"] || jsnamespace.GenderCode.UNKNOWN;
  30. };
  31. /**
  32. * 取得称谓.
  33. *
  34. * @return {String} 返回称谓字符串.
  35. */
  36. jsnamespace.PersonInfo.prototype.getAppellation = function() {
  37. var rt = "";
  38. if (jsnamespace.GenderCode.MALE == this.gender) {
  39. rt = "Mr.";
  40. } else if (jsnamespace.GenderCode.FEMALE == this.gender) {
  41. rt = "Ms.";
  42. }
  43. return rt;
  44. };
  45. /**
  46. * 取得欢迎字符串.
  47. *
  48. * @return {String} 返回欢迎字符串.
  49. */
  50. jsnamespace.PersonInfo.prototype.getHello = function() {
  51. var rt = "Hello, " + this.getAppellation() + " " + (this.name);
  52. return rt;
  53. };
  54. // == PersonInfoUtil class ==
  55. /** @class
  56. * 个人信息工具. 闭包法的类.
  57. */
  58. jsnamespace.PersonInfoUtil = function () {
  59. /**
  60. * 前缀.
  61. *
  62. * @static @private
  63. */
  64. var _prefix = "[show] ";
  65. return {
  66. /** 显示信息.
  67. *
  68. * @param {jsnamespace.PersonInfo} p 个人信息.
  69. * @static
  70. */
  71. show: function(p) {
  72. var s = _prefix;
  73. if (!!p) {
  74. s += p.getHello();
  75. }
  76. alert(s);
  77. },
  78. /** 版本号. @readonly */
  79. version: 0x100
  80. };
  81. }();

3.2 jsnamespace_sub.js

jsnamespace_sub.js演示了如何在多个文件中使用同一个顶层命名空间,并建立子命名空间。

jsnamespace.sub 命名空间里有这些类——

  • Animal: 动物. 极简主义法的类.
  • Cat: 猫. 继承自Animal. 极简主义法的类.
  1. // 声明本模块所依赖的命名空间.
  2. var jsnamespace = window.jsnamespace || {};
  3. /** @class
  4. * 子命名空间.
  5. * @abstract
  6. */
  7. jsnamespace.sub = window.jsnamespace.sub || {};
  8. // 极简主义法(minimalist approach)定义类.
  9. /**
  10. * 动物.
  11. */
  12. jsnamespace.sub.Animal = {
  13. /** 创建 动物.
  14. *
  15. * @return {Animal} 返回所创建的对象.
  16. * @static
  17. */
  18. createNew: function(){
  19. var animal = {};
  20. /** 睡觉.
  21. */
  22. animal.sleep = function(){ alert("睡懒觉"); };
  23. return animal;
  24. }
  25. };
  26. /**
  27. * 猫.
  28. * @extends jsnamespace.sub.Animal
  29. */
  30. jsnamespace.sub.Cat = {
  31. /** 声音.
  32. * @static @protected
  33. */
  34. sound : "喵喵喵",
  35. /** 创建 猫.
  36. *
  37. * @return {Cat} 返回所创建的对象.
  38. * @static
  39. */
  40. createNew: function(){
  41. var cat = jsnamespace.sub.Animal.createNew();
  42. /** 发声.
  43. */
  44. cat.makeSound = function(){ alert(jsnamespace.sub.Cat.sound); };
  45. /** 修改声音.
  46. * @param {String} x 声音.
  47. */
  48. cat.changeSound = function(x){ jsnamespace.sub.Cat.sound = x; };
  49. return cat;
  50. }
  51. };

3.3 测试页面

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml">
  3. <head>
  4. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  5. <title>测试JavaScript 命名空间</title>
  6. </head>
  7. <body>
  8. <script type="text/javascript" src="jsnamespace.js"></script>
  9. <script type="text/javascript" src="jsnamespace_sub.js"></script>
  10. <script type="text/javascript">
  11. /** 测试. */
  12. function doTest() {
  13. //alert(jsnamespace);
  14. var p1 = new jsnamespace.PersonInfo();
  15. p1.name = "Zhang San"; // 张三.
  16. p1.gender = jsnamespace.GenderCode.MALE;
  17. var p2 = new jsnamespace.PersonInfo({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE}); // 李四.
  18. jsnamespace.PersonInfoUtil.show(p1);
  19. jsnamespace.PersonInfoUtil.show(p2);
  20. //
  21. var c = jsnamespace.sub.Cat.createNew();
  22. c.makeSound();
  23. }
  24. </script>
  25. <h1>测试JavaScript 命名空间</h1>
  26. <input type="button" value="测试" OnClick="doTest();" title="doTest" />
  27. <br/>
  28. 输出:<br/>
  29. <textarea id="txtresult" rows="12" style="width:95%"></textarea>
  30. </body>
  31. </html>

四、用JSDuck生成文档

以下截图,就是JSDuck根据上面的代码所生成文档。可发现它完美的识别了代码中的命名空间(jsnamespace),并以树形展示。且类、属性、方法等的文档也正确生成了。



五、心得总结

过去为了避免全局变量污染,一般是采用立即调用函数(IIFE)法写闭包类,将私有数据封装在一个类中。但该方案有2个缺点——

  1. 为了尽可能封装、隐藏细节,可能会导致闭包内的代码行数非常多,可读性低,不易开发维护。
  2. 当代码量大、使用多个js文件时,因为闭包不能跨文件,每个js文件都至少有一个闭包类的全局变量,即还是会在全局变量中占据多个名字。这时得小心命名,避免冲突。

而现在有了统一的命名空间方案后,便可放心的将复杂的闭包类,按照“低耦合高内聚”拆分为多个小的闭包类,并挂到命名空间中(给命名空间Object的字段赋值)。

而且,因随时可以给命名空间Object增加新的字段。所以即使代码分散在多个js文件中,也能使用同一个命名空间,测底避免全局变量污染。

源码地址:

https://github.com/zyl910/test_jsduck

参考文献

JavaScript 实现命名空间(namespace)的最佳方案——兼容主流的定义类(class)的方法,兼容所有浏览器,支持用JSDuck生成文档的更多相关文章

  1. JavaScript 定义类的最佳写法——完整支持面向对象(封装、继承、多态),兼容所有浏览器,支持用JSDuck生成文档

    作者: zyl910 [TOC] 一.缘由 由于在ES6之前,JavaScript中没有定义类(class)语法.导致大家用各种五花八门的办法来定义类,代码风格不统一.而且对于模拟面向对象的三大支柱& ...

  2. JavaScript一个生成文档目录的实例

    执行结果: <body> <script type="text/javascript"> /** * 这个模块注册一个可在页面加载完成后自动运行的匿名函数, ...

  3. 系统后台图表生成文档说明-javascript

    1.引入jquery插件文件datas.js 2.各图表分类 表格 $('#'+tableId).mTable({ url:'', //数据来源,[必填] pageNum:1, //分页,默认为1,[ ...

  4. XAML实例教程系列 - 命名空间(NameSpace) 三

    XAML实例教程系列 - 命名空间(NameSpace) 2012-05-28 14:14 by jv9, 2205 阅读, 10 评论, 收藏, 编辑 上一篇曾提及XAML中,每个对象元素的声明是对 ...

  5. javascript 定义类(转载)

    Javascript本身并不支持面向对象,它没有访问控制符,它没有定义类的关键字class,它没有支持继承的extend或冒号,它也没有用来支持虚函数的virtual,不过,Javascript是一门 ...

  6. javascript定义类和类的实现

    首先说说类,在一个类里我们会有以下的几个特征: 1. 公有方法 2. 私有方法 3. 属性 4. 私有变量 5. 析构函数 我们直接看一个例子: /***定义类***/ var Class = fun ...

  7. JavaScript定义类的几种方式

    提起面向对象我们就能想到类,对象,封装,继承,多态.在<javaScript高级程序设计>(人民邮电出版社,曹力.张欣译.英文名字是:Professional JavaScript for ...

  8. Objective-C规范注释心得——同时兼容appledoc(docset、html)与doxygen(html、pdf)的文档生成

    作者:zyl910 手工写文档是一件苦差事,幸好现在有从源码中抽取注释生成文档的专用工具.对于Objective-C来说,目前最好用的工具是appledoc和doxygen.可是这两种工具对于注释的要 ...

  9. JavaScript中定义类的方式详解

    本文实例讲述了JavaScript中定义类的方式.分享给大家供大家参考,具体如下: Javascript本身并不支持面向对象,它没有访问控制符,它没有定义类的关键字class,它没有支持继承的exte ...

随机推荐

  1. JavaScript正则表达式知识点

    通过学习imooc课程<JavaScript正则表达式>http://www.imooc.com/video/12539,对视频教学内容做一个知识整理. 一个正则表达式在线工具:http: ...

  2. 【APP问题定位(三)】adb安装

    先来剧透一下我们需要使用的工具 bin包               一个安装目录,可以免安装直接调用adb命令 Android SDK platform tools 下面依次为大家介绍,第1个和第2 ...

  3. SSO单点登录一:cas单点登录防止登出退出后刷新后退ticket失效报500错,也有退出后直接重新登录报票根验证错误

    问题1: 我登录了client2,又登录了client3,现在我把client2退出了,在client3里面我F5刷新了一下,结果页面报错: 未能够识别出目标 'ST-41-2VcnVMguCDWJX ...

  4. SQL命令语句小技巧

    1.[ ]的使用 当我们所要查的表是系统关键字或者表名中含有空格时,需要用[]括起来,例如新建了两个表,分别为user,user info,那么select * from user和select * ...

  5. .net中LAMBDA表达式常用写法

    这里主要是将数据库中的常用操作用LAMBDA表达式重新表示了下,用法不多,但相对较常用,等有时间了还会扩展,并将查询语句及LINQ到时也一并重新整理下: 1.select语句:books.Select ...

  6. 如何在ASP.NET Core Web API测试中使用Postman

    使用Postman进行手动测试 如果您是开发人员,测试人员或管理人员,则在构建和使用应用程序时,有时了解各种API方法可能是一个挑战. 使用带有.NET Core的Postman为您的Web API生 ...

  7. ionic基本环境的搭建

    1.下载版本大于6的Node.js https://nodejs.org/en/ 个人喜欢下载最新版本 安装成功后可以用命令行工具输入node -v和npm -v分别查看node.npm版本 2.下载 ...

  8. Linux笔记(固定USB摄像头硬件端口,绑定前后置摄像头)

    在Android的系统会有前置摄像头和后置摄像头的定义,摄像头分为SOC类型的摄像头和USB这一类的摄像头,接下要分析就是USB摄像头这一类 . 一般在android或者linux系统中分析一个模块, ...

  9. matlab之“audioread”函数帮助文档翻译

    课 程 设 计 (1)  原文 audioread Read audio file Syntax [y,Fs] = audioread(filename) [y,Fs] = audioread(fil ...

  10. sqoop的导入导出

    1.知道某列的值的增量导入(mysql------>文件) bin/sqoop import \--connect jdbc:mysql://bigdatcdh01:3306/test \--u ...