随着网站逐渐变成互联网应用程序,嵌入网页的 Javascript 代码越来越庞大,越来越复杂,extjs就是一个很好的例子。 网页越来越像桌面程序,需要一个团队分工协作。
Javascript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。但早期Javascript不是一种模块化编程语言,不支持"类"(class),更遑论"模块"(module)了。所以js模块化开发的方式一直在改变,从 2009 年就诞生了 CommonJS 规范,在 2015 年的最新的 ES6 中,官方终于引入了对于模块的原生支持。
什么是模块化?
将一个复杂的程序,依据一定的规则(规范)封装成几个不同的模块(文件),并组合在一起,最终达到:各个模块内部数据和实现是私有的,只向外部暴露一些接口(方法)与外部其他模块通信。
模块化好处:
- 可维护性:模块是独立的(内部数据、实现是私有的),这样在修改时不会影响到其他模块;其次模块具有独立的命名空间,避免了各种冲突的情况。
- 重用代码:通过模块引用的方式,实现代码复用。
一、上古时代的js原生模块
早期将不同的功能封装到不同函数(以及记录状态的变量)中,并简单地放在一起,就算是一个“模块”。例如,把以下函数放到 一个js文件中:
function m1(){
//...
}
function m2(){
//...
}
上面的函数m1()和m2(),组成一个模块,使用的时候,引入对应js文件然后直接调用即可。在早期web开发中,通常直接在<script>标签下写js代码,因为在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限,所以这种写法依然存在使用。
缺点:(不符合模块化定义)
- 代码无法复用;可维护性差;
- "污染"了全局作用域;(因为自定义的函数、变量等都挂在了全局变量window上);
1、使用全局对象(命名空间)封装:
为了解决上面命名冲突的问题,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。例如:
var module = {
count:9,
m1:function() {
console.log("module...");
},
m2:function(str){
console.log(str);
}
};
//调用
module.count = 10;
module.m1();
module.m2("hello js");
console.log(moudle.count);//10
//-----------------------------
var module1 = {};
module1.count = 9;
module1.m1 = function() {
console.log("module1...");
};
module1.m2 = function(str) {
console.log(str);
};
//调用
module1.count = 10;
module1.m1();
module1.m2("hello js");
console.log(moudle1.count);//10
上面m1()、m2()函数以及变量count,被封装在全局对象(命名空间)module里。使用的时候,调用这个对象的属性:module1.m1(); 这种方式在一定程度上避免了全局作用域污染,因为可能其他人不知道此全局对象(命名空间)已经定义,他又定义了一次并赋值,这时就出现了覆盖。为此,可以通过如下方式避免:(也许会疑问,为什么可以再一次声明module变量?实际上js的解释执行,会把所有声明都提前。如果一个变量已经声明过,后面如果不是在函数内声明的,则是没有影响的。所以,就算在别的地方声明过var module,我同样也以可以在这里再次声明一次)
//...
var module;
if(!module) {
module = {
};
}
缺点:
- 这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值,例如:module1.count = 5;
- 性能较低:由于是在全局作用域上定义的,而js变量的调用,从全局作用域上找查的速度会比在私有作用域里面慢得多得多。我们最好将插件逻辑写在一个私有作用域中。
2、利用IIFE和闭包封装:
最好将插件逻辑写在一个私有作用域中,实现私有作用域,最好的办法就是使用IIFE+闭包。
2.1)IIFE+闭包:
在javascript中不支持private、public这类修饰符,我们可以利用IIFE(形成单独作用域来封装外部无法读取的私有变量)和闭包(闭包可以设置私有变量)来隐藏私有变量和方法,仅暴露需要对外提供的接口。例如:
//闭包
function module() {
var m = 99;
function m1() {
console.log("m1");
}
var m2 = function() {
console.log("m2",m);
}
return {
"m1":m1,
"m2":m2
}
}
//调用
var mod = module();
mod.m1();//m1
mod.m2();//m2,99
console.log(mod.m);//undefine
//IIFE+闭包
var modl = (function(){
var m = 99;
function m1() {
console.log("m1");
}
var m2 = function () {
console.log("m2",m);
}
return {
"m1":m1,
"m2":m2
}
})();
//调用
modl.m1();//m1
modl.m2();//m2,99
console.log(modl.m);//undefine
这两种写法本质都是通过闭包(返回内部函数,只不过是返回到对象中)实现隐藏私有变量,仅暴露对外提供的接口。推荐使用右边的方式,因为左边var m = module1();可以被调用任意多次,每次调用都会创建一个新的模块实例。一般来说,我们只希望有一个实例,所以可以通过IIFE来实现单例模式。
IIFE+闭包实现模块的最常用写法:
(function(win){
var m = 99;
function m1() {
console.log("m1");
}
var m2 = function() {
console.log("m2",m);
}
win.module = {
"m1":m1,
"m2":m2
}
})(window);
//调用
module.m1();//m1
module.m2();//m2,99
console.log(module.m);//undefine
注:IIFE中通过window.fn = fn来暴露接口,而这个fn就是闭包(同时符合定义2、3:window.fn可以访问IIFE作用域中定义的变量和方法;同时,window.fn的调用在声明fn作用域之外),而IIFE只是一个包含闭包的函数调用。
2.2)放大模式:
我们知道js的继承是用原型链来实现的,但是这里要讨论的是模块的扩展,所以这边不会讨论继承问题。如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1);
将原来的module1模块作为IIFE参数传入,并添加了一个新方法m3(),然后返回module1模块。
2.3)宽放大模式:
在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。
var module1 = ( function (mod){
//...
return mod;
})(window.module1 || {});
与"放大模式"相比,"宽放大模式"就是"立即执行函数"的参数可以是空对象。
2.4)独立性:
独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
var module1 = (function ($, YAHOO) {
//...
})(jQuery, YAHOO);
上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。这方面更多的讨论,参见Ben Cherry的著名文章《JavaScript Module Pattern: In-Depth》。
2.5)解决依赖:
假设我们模块需要依赖jQuery,可以通过传参方式解决,如下:
(function(win,$){
var m = 99;
function m1() {
$(body).css("background-color","yellow");
console.log("m1",m);
}
function m2() {
console.log("m2");
}
win.modul2 = {
"m1":m1,
"m2":m2
}
})(window,jQuery);
注:jQuery是jQuery库暴露的对象,所以必须在模块之前,先要将jQuery库加载进来。
说明:2.2)到2.5)本质上都是给IIFE传递参数。
优点:“IIFE+闭包”很好的实现了模块化(私有、可以暴露共有api),实际上在ES6的modules之前,原生js利用“IIFE+闭包”这种方式实现模块化是非常常见的。
缺点:如果多个模块之间有相互依赖,整个系统会变得很复杂。
- 引入多个js会导致请求过多;
- 依赖模糊;
- 难以维护;
这时候就需要CommonJS、AMD、ES6等这种规范模块化的解决方案了。
二、【示例】原生js使用IIFE+闭包编写模块
目录结构:
/
└─ iife_module
├─ js
│ ├─ alertService.js
│ └─ dataService.js
├─ app.js
└─ index.html
1)dataService.js 没有依赖的模块
(function(win){
var name = "data_service module";
function getName() {
return name;
}
win.dataService = {
"getName":getName
};
})(window);
2)alertService.js 有依赖模块
(function(win,dataService){
var msg = "alert_service module";
function showMsg() {
let name = dataService.getName();
console.log(msg,name);
}
win.alertService = {
"showMsg":showMsg
};
})(window,dataService);
3)app.js 主模块
(function(alertService){
alertService.showMsg();
})(alertService);
4)index.html
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<script src="./js/dataService.js"></script>
<script src="./js/alertService.js"></script>
<script src="./app.js"></script>
</body>
</html>