最近闲下来的时候,稍微想了想这个问题。关于Javascript模块化和命名空间管理

【关于模块化以及为什么要模块化】

先说说我们为什么要模块化吧。其实这还是和编码思想和代码管理的便利度相关(没有提及名字空间污染的问题是因为我相信已经考虑到模块化思想的编码者应该至少有了一套自己的命名法则,在中小型的站点中,名字空间污染的概率已经很小了,但也不代表不存在,后面会说这个问题)。 
其实模块化思想还是和面向对象的思想如出一辙,只不过可能我们口中所谓的“模块”是比所谓的“对象”更大的对象而已。我们把致力完成同一个目的的功能函数通过良好的封装组合起来,并且保证其良好的复用性,我们大概可以把这样一个组合代码片段的思想称为面向对象的思想。这样做的好处有很多,比如:易用性,通用性,可维护性,可阅读性,规避变量名污染等等。 
而模块化无非就是在面向对象上的面向模块而已,我们把和同一个项目(模块)相关的功能封装有机的组合起来,通过一个共同的名字来管理。就大概可以说是模块化的思想。所以,相比面向对象而言的话,我觉得在代码架构上贯彻模块化的思想其实比面向对象的贯彻还更为容易一些。 
不像c#,java等这种本身就拥有良好模块化和命名空间机制的强类型语言。JavaScript并没有为创建和管理模块而提供任何语言功能。正因为这样,我们在做js的编码的某些时候,对于所谓的命名空间(namespace)的使用会显得有些过于随便(包括我自己)。比如 :

  1. var Hongru = {} // namespace
  2.  
  3. (function(){
  4. Hongru.Class1 = function () {
  5. //TODO
  6. }
  7. ...
  8. Hongru.Class2 = function () {
  9. //TODO
  10. }
  11. })();

如上,我们通常用一个全局变量或者全局对象就作为我们的namespace,如此简单,甚至显得有些随便的委以它这么重大的责任。但是我们能说这样做不好吗?不能,反而是觉得能有这种编码习惯的同学应该都值得表扬。。。

所以,我们在做一些项目的时候或者建一些规模不大的网站时,简单的用这种方式来做namespace的工作其实也够了,基本不会出什么大乱子。但是回归本质,如果是有代码洁癖或者是建立一个大规模的网站,抑或一开始就抱着绝对优雅的态度和逻辑来做代码架构的话。或许我们该考虑更好一些的namespace的注册和管理方式。 
在这个方面,jQuery相比于YUI,Mootool,EXT等,就显得稍逊一筹,(虽然jq也有自己的一套模块化机制),但这依然不妨碍我们对它的喜爱,毕竟侧重点不同,jq强是强在它的选择器,否则他也不会叫j-Query了。 
所以我们说jQuery比较适合中小型的网站也不无道理。就像豆瓣的开源的前端轻量级框架Do框架一样,也是建立在jQuery上,封装了一层模块化管理的思想和文件同步载入的功能。

【关于namespace】

好了,我们回归正题,如上的方式,简单的通过全局对象来做namespace已经能够很好的减少全局变量,规避变量名污染的问题,但是一旦网站规模变大,或者项目很多的时候,管理多个全局对象的名字空间依然会有问题。如果不巧发生了名字冲突,一个模块就会覆盖掉另一个模块的属性,导致其一或者两者都不能正常工作。而且出现问题之后,要去甄别也挺麻烦。所以我们可能需要一套机制或者工具,能在创建namespace的时候就能判断是否已有重名。

另一方面,不同模块,亦即不同namespace之间其实也不能完全独立,有时候我们也需要在不同名字空间下建立相同的方法或属性,这时方法或属性的导入和导出也会是个问题。

就以上两个方面,我稍微想了想,做了些测试,但依然有些纰漏。今天又重新翻了一下“犀牛书”,不愧是经典,上面的问题,它轻而易举就解决了。基于“犀牛书”的解决方案和demo,我稍微做了些修改和简化。把自己的理解大概分享出来。比较重要的有下面几个点:

--测试每一个子模块的可用性

由于我们的名字空间是一个对象,拥有对象应该有的层级关系,所以在检测名字空间的可用性时,需要基于这样的层级关系去判断和注册,这在注册一个子名字空间(sub-namespace)时尤为重要。比如我们新注册了一个名字空间为Hongru,然后我们需要再注册一个名字空间为Hongru.me,亦即我们的本意就是me这个namespace是Hongru的sub-namespace,他们应该拥有父子的关系。所以,在注册namespace的时候需要通过‘.'来split,并且进行逐一对应的判断。所以,注册一个名字空间的代码大概如下:

  1. // create namespace --> return a top namespace
  2. Module.createNamespace = function (name, version) {
  3. if (!name) throw new Error('name required');
  4. if (name.charAt(0) == '.' || name.charAt(name.length-1) == '.' || name.indexOf('..') != -1) throw new Error('illegal name');
  5.  
  6. var parts = name.split('.');
  7.  
  8. var container = Module.globalNamespace;
  9. for (var i=0; i<parts.length; i++) {
  10. var part = parts[i];
  11. if (!container[part]) container[part] = {};
  12. container = container[part];
  13. }
  14.  
  15. var namespace = container;
  16. if (namespace.NAME) throw new Error('module "'+name+'" is already defined');
  17. namespace.NAME = name;
  18. if (version) namespace.VERSION = version;
  19.  
  20. Module.modules[name] = namespace;
  21. return namespace;
  22. };

注:上面的Module是我们来注册和管理namespace的一个通用Module,它本身作为一个“基模块”,拥有一个modules的模块队列属性,用来存储我们新注册的名字空间,正因为有了这个队列,我们才能方便的判断namespace时候已经被注册:

  1. var Module;
  2. //check Module --> make sure 'Module' is not existed
  3. if (!!Module && (typeof Module != 'object' || Module.NAME)) throw new Error("NameSpace 'Module' already Exists!");
  4.  
  5. Module = {};
  6.  
  7. Module.NAME = 'Module';
  8. Module.VERSION = 0.1;
  9.  
  10. Module.EXPORT = ['require',
  11. 'importSymbols'];
  12.  
  13. Module.EXPORT_OK = ['createNamespace',
  14. 'isDefined',
  15. 'modules',
  16. 'globalNamespace'];
  17.  
  18. Module.globalNamespace = this;
  19.  
  20. Module.modules = {'Module': Module};

上面代码最后一行就是一个namespace队列,所有新建的namespace都会放到里面去。结合先前的一段代码,基本就能很好的管理我们的名字空间了,至于Module这个“基模块”还有些EXPORT等别的属性,等会会接着说。下面是一个创建名字空间的测试demo

  1. Module.createNamespace('Hongru', 0.1);//注册一个名为Hongru的namespace,版本为0.1

上面第二个版本参数也可以不用,如果你不需要版本号的话。在chrome-debugger下可以看到我们新注册的名字空间

可以看到新注册的Hongru命名空间已经生效:再看看Module的模块队列:

可以发现,新注册的Hongru也添进了Module的modules队列里。大家也发现了,Module里还有require,isDefined,importSymbols几个方法。
由于require(检测版本),isDefined(检测namespace时候已经注册)这两个方法并不难,就稍微简略点:

--版本和重名检测

  1. // check name is defined or not
  2. Module.isDefined = function (name) {
  3. return name in Module.modules;
  4. };
  5. // check version
  6. Module.require = function (name, version) {
  7. if (!(name in Module.modules)) throw new Error('Module '+name+' is not defined');
  8. if (!version) return;
  9. var n = Module.modules[name];
  10. if (!n.VERSION || n.VERSION < version) throw new Error('version '+version+' or greater is required');
  11. };

上面两个方法都很简单,相信大家都明白,一个是队列检测是否重名,一个检测版本是否达到所需的版本。也没有什么特别的地方,就不细讲了,稍微复杂一点的是名字空间之间的属性或方法的相互导入的问题。 
--名字空间中标记的属性或方法的导出 
由于我们要的是一个通用的名字空间注册和管理的tool,所以在做标记导入或导出的时候需要考虑到可配置性,不能一股脑全部导入或导出。所以就有了我们看到的Module模板中的EXPORT和EXPORT_OK两个Array作为存贮我们允许导出的属性或方法的标记队列。其中EXPORT为public的标记队列,EXPORT_OK为我们可以自定义的标记队列,如果你觉得不要分这么清楚,也可以只用一个标记队列,用来存放你允许导出的标记属性或方法。 
有了标记队列,我们做的导出操作就只针对EXPORT和EXPORT_OK两个标记队列中的标记。

  1. / import module
  2. Module.importSymbols = function (from) {
  3. if (typeof form == 'string') from = Module.modules[from];
  4. var to = Module.globalNamespace; //dafault
  5. var symbols = [];
  6. var firstsymbol = 1;
  7. if (arguments.length>1 && typeof arguments[1] == 'object' && arguments[1] != null) {
  8. to = arguments[1];
  9. firstsymbol = 2;
  10. }
  11. for (var a=firstsymbol; a<arguments.length; a++) {
  12. symbols.push(arguments[a]);
  13. }
  14. if (symbols.length == 0) {
  15. //default export list
  16. if (from.EXPORT) {
  17. for (var i=0; i<from.EXPORT.length; i++) {
  18. var s = from.EXPORT[i];
  19. to[s] = from[s];
  20. }
  21. return;
  22. } else if (!from.EXPORT_OK) {
  23. // EXPORT array && EXPORT_OK array both undefined
  24. for (var s in from) {
  25. to[s] = from[s];
  26. return;
  27. }
  28. }
  29. }
  30. if (symbols.length > 0) {
  31. var allowed;
  32. if (from.EXPORT || form.EXPORT_OK) {
  33. allowed = {};
  34. if (from.EXPORT) {
  35. for (var i=0; i<form.EXPORT.length; i++) {
  36. allowed[from.EXPORT[i]] = true;
  37. }
  38. }
  39. if (from.EXPORT_OK) {
  40. for (var i=0; i<form.EXPORT_OK.length; i++) {
  41. allowed[form.EXPORT_OK[i]] = true;
  42. }
  43. }
  44. }
  45. }
  46. //import the symbols
  47. for (var i=0; i<symbols.length; i++) {
  48. var s = symbols[i];
  49. if (!(s in from)) throw new Error('symbol '+s+' is not defined');
  50. if (!!allowed && !(s in allowed)) throw new Error(s+' is not public, cannot be imported');
  51. to[s] = form[s];
  52. }
  53. }
  54.  
  55. 这个方法中第一个参数为导出源空间,第二个参数为导入目的空间(可选,默认是定义的globalNamespace),后面的参数也是可选,为你想导出的具体属性或方法,默认是标记队列里的全部。
  56. 下面是测试demo
  57. 复制代码 代码如下:
  58.  
  59. Module.createNamespace('Hongru');
  60. Module.createNamespace('me', 0.1);
  61. me.EXPORT = ['define']
  62. me.define = function () {
  63. this.NAME = '__me';
  64. }
  65. Module.importSymbols(me, Hongru);//把me名字空间下的标记导入到Hongru名字空间下

可以看到测试结果:

本来定义在me名字空间下的方法define()就被导入到Hongru名字空间下了。当然,这里说的导入导出,其实只是copy,在me名字空间下依然能访问和使用define()方法。

好了,大概就说到这儿吧,这个demo也只是提供一种管理名字空间的思路,肯定有更加完善的方法,可以参考下YUI,EXT等框架。或者参考《JavaScript权威指南》中模块和名字空间那节。

最后贴下源码:

  1. /* == Module and NameSpace tool-func ==
  2. * author : hongru.chen
  3. * date : 2010-12-05
  4. */
  5. var Module;
  6. //check Module --> make sure 'Module' is not existed
  7. if (!!Module && (typeof Module != 'object' || Module.NAME)) throw new Error("NameSpace 'Module' already Exists!");
  8. Module = {};
  9. Module.NAME = 'Module';
  10. Module.VERSION = 0.1;
  11. Module.EXPORT = ['require',
  12. 'importSymbols'];
  13. Module.EXPORT_OK = ['createNamespace',
  14. 'isDefined',
  15. 'modules',
  16. 'globalNamespace'];
  17. Module.globalNamespace = this;
  18. Module.modules = {'Module': Module};
  19. // create namespace --> return a top namespace
  20. Module.createNamespace = function (name, version) {
  21. if (!name) throw new Error('name required');
  22. if (name.charAt(0) == '.' || name.charAt(name.length-1) == '.' || name.indexOf('..') != -1) throw new Error('illegal name');
  23. var parts = name.split('.');
  24. var container = Module.globalNamespace;
  25. for (var i=0; i<parts.length; i++) {
  26. var part = parts[i];
  27. if (!container[part]) container[part] = {};
  28. container = container[part];
  29. }
  30. var namespace = container;
  31. if (namespace.NAME) throw new Error('module "'+name+'" is already defined');
  32. namespace.NAME = name;
  33. if (version) namespace.VERSION = version;
  34. Module.modules[name] = namespace;
  35. return namespace;
  36. };
  37. // check name is defined or not
  38. Module.isDefined = function (name) {
  39. return name in Module.modules;
  40. };
  41. // check version
  42. Module.require = function (name, version) {
  43. if (!(name in Module.modules)) throw new Error('Module '+name+' is not defined');
  44. if (!version) return;
  45. var n = Module.modules[name];
  46. if (!n.VERSION || n.VERSION < version) throw new Error('version '+version+' or greater is required');
  47. };
  48. // import module
  49. Module.importSymbols = function (from) {
  50. if (typeof form == 'string') from = Module.modules[from];
  51. var to = Module.globalNamespace; //dafault
  52. var symbols = [];
  53. var firstsymbol = 1;
  54. if (arguments.length>1 && typeof arguments[1] == 'object' && arguments[1] != null) {
  55. to = arguments[1];
  56. firstsymbol = 2;
  57. }
  58. for (var a=firstsymbol; a<arguments.length; a++) {
  59. symbols.push(arguments[a]);
  60. }
  61. if (symbols.length == 0) {
  62. //default export list
  63. if (from.EXPORT) {
  64. for (var i=0; i<from.EXPORT.length; i++) {
  65. var s = from.EXPORT[i];
  66. to[s] = from[s];
  67. }
  68. return;
  69. } else if (!from.EXPORT_OK) {
  70. // EXPORT array && EXPORT_OK array both undefined
  71. for (var s in from) {
  72. to[s] = from[s];
  73. return;
  74. }
  75. }
  76. }
  77. if (symbols.length > 0) {
  78. var allowed;
  79. if (from.EXPORT || form.EXPORT_OK) {
  80. allowed = {};
  81. if (from.EXPORT) {
  82. for (var i=0; i<form.EXPORT.length; i++) {
  83. allowed[from.EXPORT[i]] = true;
  84. }
  85. }
  86. if (from.EXPORT_OK) {
  87. for (var i=0; i<form.EXPORT_OK.length; i++) {
  88. allowed[form.EXPORT_OK[i]] = true;
  89. }
  90. }
  91. }
  92. }
  93. //import the symbols
  94. for (var i=0; i<symbols.length; i++) {
  95. var s = symbols[i];
  96. if (!(s in from)) throw new Error('symbol '+s+' is not defined');
  97. if (!!allowed && !(s in allowed)) throw new Error(s+' is not public, cannot be imported');
  98. to[s] = form[s];
  99. }
  100. }

关于Javascript模块化和命名空间管理的问题说明的更多相关文章

  1. 《前端之路》之 Javascript 模块化管理的来世今生

    目录 第二章 - 04: Javascript 模块化管理的来世今生 一.什么是模块化开发 1-1.模块化第一阶段 1-2.封装到对象 1-3. 对象的优化 二.模块化管理的发展历程 2-1.Comm ...

  2. Javascript模块化开发,使用模块化脚本加载工具RequireJS,提高你代码的速度和质量。

    随着前端JavaScript代码越来越重,如何组织JavaScript代码变得非常重要,好的组织方式,可以让别人和自己很好的理解代码,也便于维护和测试.模块化是一种非常好的代码组织方式,本文试着对Ja ...

  3. Javascript模块化编程之路——(require.js)

    转自:http://www.ruanyifeng.com/blog/2012/10/javascript_module.html Javascript模块化编程(一):模块的写法 随着网站逐渐变成&q ...

  4. JavaScript 模块化简述

    JavaScript 模块化简述 前言 关于模块化,最直接的表现就是我们写的 require 和 import 关键字,如果查阅相关资料,就一定会遇到 CommonJS .CMD AMD 这些名词,以 ...

  5. JavaScript 模块化

    当项目越来越大时,会遇到一些问题: 1.命名冲突 2.文件依赖 所有就有了javascript模块化开发概念. 模块化开发的演变: 1.函数块:最开始用全局函数将代码块包括在函数体内,然后把很多函数写 ...

  6. JavaScript 模块化简析

    关于模块化,最直接的表现就是我们写的 require 和 import 关键字,如果查阅相关资料,就一定会遇到 CommonJS .CMD AMD 这些名词,以及 RequireJS.SeaJS 等陌 ...

  7. JavaScript模块化演变 CommonJs,AMD, CMD, UMD(一)

    原文链接:https://www.jianshu.com/p/33d53cce8237 原文系列2链接:https://www.jianshu.com/p/ad427d8879cb 前端完全手册: h ...

  8. Javascript模块化编程-require.js

    转自:https://www.cnblogs.com/digdeep/p/4607131.html Javascript模块化编程(一):模块的写法 随着网站逐渐变成"互联网应用程序&quo ...

  9. 看完我的笔记不懂也会懂----javascript模块化

    JavaScript模块化 模块化引子 模块化的历史进化 模块化规范 CommonJS规范 Node.js(服务器端) 下项目的结构分析 browerify(浏览器端) 下项目的结构分析 AMD规范 ...

随机推荐

  1. Java高质量代码之 — 泛型与反射

    在Java5后推出了泛型,使我们在编译期间操作集合或类时更加的安全,更方便代码的阅读,而让身为编译性语言的Java提供动态性的反射技术,更是在框架开发中大行其道,从而让Java活起来,下面看一下在使用 ...

  2. 析构函数的调用------新标准c++程序设计

    示例1: #include<iostream> using namespace std; class CDemo{ public: ~CDemo(){cout<<"d ...

  3. cinder create volume的流程-scheduler调度

    创建 Volume 时,cinder-scheduler 会基于容量.Volume Type 等条件选择出最合适的存储节点,然后让其创建 Volume. 1.cinder-scheduler配置相关项 ...

  4. 6w5:第六周程序填空题2

    描述 下面程序的输出结果是: destructor B destructor A 请完整写出 class A. 限制条件:不得为 class A 编写构造函数. #include <iostre ...

  5. 串的模式之kmp算法实践题

    给定两个由英文字母组成的字符串 String 和 Pattern,要求找到 Pattern 在 String 中第一次出现的位置,并将此位置后的 String 的子串输出.如果找不到,则输出“Not ...

  6. 【guava】对象处理

    一,equals方法 我们在开发中经常会需要比较两个对象是否相等,这时候我们需要考虑比较的两个对象是否为null,然后再调用equals方法来比较是否相等,google guava库的com.goog ...

  7. QT中QWidget、QDialog以及MainWindow的区别

    参考 http://blog.csdn.net/u011619422/article/details/47311101 QT中QWidget.QDialog以及MainWindow的区别 QWidge ...

  8. Spring学习笔记(一)—— Spring介绍及入门案例

    一.Spring概述 1.1 Spring是什么 Spring是一个开源框架,是于2003年兴起的一个轻量级的Java开发框架, 由Rod Johnson 在其著作<Expert one on ...

  9. Karma+Jasmine测试环境搭建

    1.如果你还没安装node的话,去这里下载:http://nodejs.cn/download/,选择跟你电脑匹配的并进行安装,一路next下来就行,路径最好改成自己让自己舒服的,默认的路径可能会很让 ...

  10. 114th LeetCode Weekly Contest Array of Doubled Pairs

    Given an array of integers A with even length, return true if and only if it is possible to reorder ...