您的位置:首页 > Web前端 > JavaScript

JavaScript 模块化编程 - Module Pattern

2016-12-16 10:31 309 查看

## 前言

The Module Pattern,模块模式,也译为模组模式,是一种通用的对代码进行模块化组织与定义的方式。这里所说的模块(Modules),是指实现某特定功能的一组方法和代码。许多现代语言都定义了代码的模块化组织方式,比如 Golang 和 Java,它们都使用 package 与 import 来管理与使用模块,而目前版本的 JavaScript 并未提供一种原生的、语言级别的模块化组织模式,而是将模块化的方法交由开发者来实现。因此,出现了很多种 JavaScript 模块化的实现方式,比如,CommonJS
Modules、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 方法。不过,它也可以是个不错的参考。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息