JavaScript 模块化编程 - Module Pattern
2016-12-16 10:31
309 查看
## 前言
The Module Pattern,模块模式,也译为模组模式,是一种通用的对代码进行模块化组织与定义的方式。这里所说的模块(Modules),是指实现某特定功能的一组方法和代码。许多现代语言都定义了代码的模块化组织方式,比如 Golang 和 Java,它们都使用 package 与 import 来管理与使用模块,而目前版本的 JavaScript 并未提供一种原生的、语言级别的模块化组织模式,而是将模块化的方法交由开发者来实现。因此,出现了很多种 JavaScript 模块化的实现方式,比如,CommonJSModules、AMD 等。
以 AMD 为例,该规范使用 define 函数来定义模块。使用 AMD 规范进行模块化编程是很简单的,大致上的结构是这样的:
define(factory(){ // 模块代码 // return something; });
目前尚在制定中的 Harmony/ECMAScript 6(也称为 ES.next),会对模块作出语言级别的定义,但距离实用尚遥不可及,这里暂时不讨论它。
作为一种模式,模块模式其实一直伴随着 JavaScript 存在,与 ES 6 无关。最近我需要重构自己的一些代码,因此我参考和总结了一些实用的模块化编程实践,以便更好的组织我的代码。需要注意的是,本文只是个人的一个总结,比较简单和片面,详尽的内容与剖析请参看文后的参考资料,它们写得很好。本文并不关心模块如何载入,只关心现今该如何组织模块化的代码。还有,不必过于纠结所谓的模式,真正重要的其实还是模块代码及思想。所谓模式,不过是我们书写代码的一些技巧和经验的总结,是一些惯用法,实践中应灵活运用。
!function (参数) { // 代码 // return something }(参数);
还有些人喜欢用括号将整个 IIFE 围起来,这样就变成了以下的形式:
(function (参数) { // 代码 // return something }(参数));
注2:在有些人的代码中,将 undefined 作为上面代码中的一个参数,他们那样做是因为 undefined 并不是 JavaScript 的保留字,用户也可以定义它,这样,当判断某个值是否是 undefined 的时候,判断可能会是错误的。将 undefined 作为一个参数传入,是希望代码能按预期那样运行。不过我认为,一般情况下那样做并没太大意义。
var MODULE = (function () { var my = {}, privateVariable = 1; function privateMethod< 153c5 span class="hljs-params">() { // ... } my.moduleProperty = 1; my.moduleMethod = function () { // ... }; return my; }());
这段代码声明了一个变量 MODULE,它带有两个可访问的属性:moduleProperty 和 moduleMethod,其它的代码都封装在闭包中保持着私有状态。参考以前提过的参数输入,我们还可以通过参数引用其它全局变量。
var Widget1 = { name: "who am i?", settings: { x: 0, y: 0 }, call_me: function () { // ... } };
有一篇文章讲解了这种形式: How Do You Structure JavaScript? The Module Pattern Edition
不过这只是一种简单的形式,你可以将它看作是模块模式的一种基础的简单表达形式,而把闭包形式看作是对它的一个封装。
var MODULE = (function () { // 私有变量及函数 var x = 1; function f1() {} function f2() {} return { public_method1: f1, public_method2: f2 }; }());
var MODULE = (function (my) { // add capabilities... return my; }(MODULE || {}));
var MODULE_TWO = (function (old) { var my = {}, key; for (key in old) { if (old.hasOwnProperty(key)) { my[key] = old[key]; } } var super_moduleMethod = old.moduleMethod; my.moduleMethod = function () { // override method on the clone, access to super through super_moduleMethod }; return my; }(MODULE));
有时我们需要复制和继承原对象,上面的代码演示了这种操作,但未必完美。如果你可以使用 Object.create() 的话,请使用 Object.create() 来改写上面的代码:
var MODULE_TWO = (function (old) { var my = Object.create(old); var super_moduleMethod = old.moduleMethod; my.moduleMethod = function () { // override method ... }; return my; }(MODULE));
var MODULE = (function () { var my = {}; // 代码 ... if (typeof define == 'function') { define( function(){ return my; } ); }else if (typeof module != 'undefined' && module.exports) { module.exports = my; } return my; }());
上面的代码在返回 my 对象之前,先检测自己是否是运行在 AMD 环境之中(检测 define 函数是否有定义),如果是,就使用 define 来定义模块,否则,继续检测是否运行于 CommonJS 中,比如 NodeJS,如果是,则将 my 赋值给 module.exports。因此,这段代码应该可以同时运行于 AMD、CommonJS 以及一般的环境之中。另外,我们的这种写法应该也可在 SeaJS 中正确执行。
(function (root, factory) { if (typeof exports === "object" && exports) { factory(exports); // CommonJS } else { var mustache = {}; factory(mustache); if (typeof define === "function" && define.amd) { define(mustache); // AMD } else { root.Mustache = mustache; // <script> } } }(this, function (mustache) { // 模块主要的代码放在这儿 });
这段代码与前面介绍的方式不太一样,它使用了两个匿名函数。后面那个函数可以看作是模块代码的工厂函数,它是模块的主体部分。前面那个函数对运行环境进行检测,根据检测的结果对模块的工厂函数进行调用。另外,作为一个通用库,它并没使用 window 对象,而是使用了 this,因为在简单的函数调用中,this 其实就是全局对象。
再看看 doT 的做法。doT 的做法与 Mustache 不同,而是更接近于我在前面介绍 AMD 环境探测的那段代码:
(function() { "use strict"; var doT = { version: '1.0.0', templateSettings: { /*...*/ }, template: undefined, //fn, compile template compile: undefined //fn, for express }; if (typeof module !== 'undefined' && module.exports) { module.exports = doT; } else if (typeof define === 'function' && define.amd) { define(function(){return doT;}); } else { (function(){ return this || (0,eval)('this'); }()).doT = doT; } // ... }());
这段代码里的 (0, eval)('this') 是一个小技巧,这个表达式用来得到 Global 对象,'this' 其实是传递给 eval 的参数,但由于 eval 是经由 (0, eval) 这个表达式间接得到的,因此 eval 将会在全局对象作用域中查找 this,结果得到的是全局对象。若是代码运行于浏览器中,那么得到的其实是 window 对象。这里有一个针对它的讨论: http://stackoverflow.com/questions/14119988/return-this-0-evalthis/14120023#14120023
其实也有其它办法来获取全局对象的,比如,使用函数的 call 或 apply,但不给参数,或是传入 null:
var global_object = (function(){ return this; }).call();
你可以参考这篇文章: Javascript的this用法
Juicer 则没有检测 AMD,它使用了如下的语句来检测 CommonJS Modules:
typeof(module) !== 'undefined' && module.exports ? module.exports = juicer : this.juicer = juicer;
另外,你还可以参考一下这个: https://gist.github.com/kitcambridge/1251221
(function (root, Library) { // The square bracket notation is used to avoid property munging by the Closure Compiler. if (typeof define == "function" && typeof define["amd"] == "object" && define["amd"]) { // Export for asynchronous module loaders (e.g., RequireJS, `curl.js`). define(["exports"], Library); } else { // Export for CommonJS environments, web browsers, and JavaScript engines. Library = Library(typeof exports == "object" && exports || (root["Library"] = { "noConflict": (function (original) { function noConflict() { root["Library"] = original; // `noConflict` can't be invoked more than once. delete Library.noConflict; return Library; } return noConflict; })(root["Library"]) })); } })(this, function (exports) { // ... return exports; });
我觉得这个写得有些复杂了,我也未必需要我的库带有 noConflict 方法。不过,它也可以是个不错的参考。
相关文章推荐
- JavaScript 模块化编程 - Module Pattern
- JavaScript 模块化编程 - Module Pattern
- JavaScript 模块化编程 - Module Pattern
- [技巧] Javascript模块化编程(三):require.js的用法
- Javascript模块化编程(二):AMD规范
- 【转】Javascript模块化编程(一):模块的写法
- Javascript模块化编程系列二: 模块化的标准化(CommonJS & AMD)
- Javascript模块化编程系列一: 模块化的驱动
- Javascript模块化编程(一):模块的写法
- 转:javascript模块化编程(require.js)
- Javascript模块化编程(三)require.js的用法及功能介绍
- Javascript模块化编程系列三: CommonJS & AMD 模块化规范描述
- Javascript模块化编程(三):require.js的用法
- Javascript模块化编程(三):模块化编程实战,试用SeaJS
- Javascript模块化编程(三):模块化编程实战,试用SeaJS
- Javascript 模块化编程
- Javascript模块化编程(一):模块的写法(推荐阅读)
- JavaScript模块化编程(一):模块原型和理论概念详解
- Javascript模块化编程(三)require.js的用法及功能介绍
- 【转】Javascript模块化编程(三):require.js的用法