javascript模块化1

随着网站逐渐变成互联网应用程序,嵌入网页的 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>

猜你喜欢

转载自blog.csdn.net/liuxiao723846/article/details/119750557