模块化下的设计模式


模块化:

模块化意味着应用程序高度解耦并且它是由一些列存储于模块中的高度解耦、不同功能的片段组成

CommonJs(更适用于服务端)
代表: node.js
eg:

1
2
var math = require("math");
math.add(1,2);

对于浏览器来说,math.add 想要运行必须要等待 math.js 给加载完成才可,而在服务端,因为所有模块都放在硬盘,可以同时加载完成,等待的只是硬盘读取的时间,而对于浏览器来说,等待的是网速,因此浏览器可能长时间处于假死状态。

写法:

1
2
3
4
5
//testModule.js
function testModule() {
...
}
module.exports = testModule;

使用 testModule 方法:

1
2
var testModule = require('testModule.js');
testModule();

CommonJS 采用服务器优先方法,假定同步行为,没有全局概念。

AMD
代表: Angular
写法:
AMD 相对 CommonJS 采用浏览器优先的开发方法,选择异步行为和简化的向后兼容性

  • 定义模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //myModule.js
    define(['foo', 'bar'], function(foo, bar) {
    var myModule = {
    sayHello: function {
    alert('Hello Eyesim');
    }
    };
    return myModule;
    })
  • 加载模块
    这里的 require([module], callback) 与 CommonJS 不同于 ADM 的 require 是有两个参数的,第一个是依赖的模块数组,第二个是加载完模块后模块执行的函数。也就是说:AMD规范则是非同步加载模块,允许指定回调函数。对于浏览器环境来说,要从服务器加在模块,这个时候必须采用非同步模式,所以浏览器环境更适合于使用 AMD。

1
2
require(['src/myModule'], function(myModule){
})

ES6
参考:阮一峰 Module 的语法
在 ES6 之前,JS 一直没有模块体系,没法将一个大程序拆分成相互依赖的解耦的模块,CommonJS 与 AMD 出现解决了部分问题,但这两个方案一个用在服务器端一个用在客户端,而 ES6 的出现,可以完全取代了 CommonJS 和 AMD 规范,成为浏览器与服务器通用的模块解决方法。
但在 ES6 标准里,并没有将 require 、exports对象写进标准,而是用 import 、以及 export 指令代替。那这里的 ES6 的写法和原本的 CommmonJS 以及 AMD 的写法有什么不同呢?

写法上:

  • 规定模块对外的接口(导出接口):

    1.声明时导出
    eg:

    1
    2
    3
    4
    5
    6
    7
    8
    //func.js
    export function a() {
    ...
    };
    export var b = {
    ...
    };
    export let c = ...;

    2.以对象形式导出

    1
    2
    3
    4
    5
    6
    7
    var d = 1;
    export {a};//类似于 {a:a}

    function e() {
    ...
    }
    export {e};

另外,export 还可以使用 as 关键词对接口进行重命名:

1
2
3
4
5
6
7
8
9
10
function f1() {
...
}
function f2() {
...
}
export {
f1 as funcF1,
f2 as funcF2
};

注意一点: export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应的关系。所以下面这种写法是错的:

1
2
3
4
export 1;//报错

var num2 = 1;
export num2;//报错

上面的写法里面,1 是一个值,不是接口,应该像下面这种写法才对:

1
2
3
4
5
6
7
8
//number.js
export var num1 =1;

var num2 = 1;
export {num2};//等价于 {num2: num2}

var num3 = 1;
export {num3 as num3};

  • 模块加载
    模块加载上,ES6 采取的是 import 命令。
    eg:
    1
    2
    3
    4
    5
    import { num1, num2 } from './number'
    function add() {
    var sum = num1 + num2;
    return sum;
    }

另外,想要给引入的方法或变量重命名的话,也可以使用关键字 as :
eg:

1
import { num1 as n1 } from './number'

import 后面的 from 指定模块文件的位置,可以是相对路径,也可以是绝对路径, .js 的后缀可以省略,如果只是模块名,不带路径,但是必须要有配置文件,告诉 JavaScript 引擎该模块的位置。
注意一点:
import 命令具有提升效果,会提升到整个模块的头部,首先执行。

1
2
f1();
import { a as f1 } from './func'

另外, import 是静态执行,不能使用表达式与变量,否则会报错。

1
2
3
4
5
6
7
8
9
10
11
import { 'num' +'1' } from './number';//报错

let numModule = './number';
import { num1 } from numModule;//报错

//报错
if( flag === 1 ) {
import { num1 as num } from './number';
}else{
import { num2 as num } from './number';
}

最后,如果多次执行同一句语句或加载同一个模块,那么只会执行一次加载。

eg:

1
2
3
4
5
6
7
//多次重复的 import 语句,只会执行一次。
import 'util';
import 'util';

//多次加载同一个模块,只会执行一次
import { num1 } from './number';
import { num2 } from './number';

取值上:

  • export 输出的接口与其对应的值是动态绑定关系

也就是说,通过该接口可以取到模块内部实时的值。而 CommonJS 模块取出的值是缓存的,不存在动态更新。
另外, export 命令可以放到模块的任何地方,只要处于模块顶层就可以了。如果处于块级作用域里面则会报错。
eg:

1
2
3
4
function f1() {
export default 'funcF1';//报错
}
f1();

这是由于处于代码块中,就没法做静态优化了,ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。而 CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

eg:

1
2
3
4
5
6
7
8
// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等价于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

CommonJS 实际上是整体加载 fs 模块然后生成一个 _fs 对象,然后再从这个对象上面读取 3 个方法,这种加载模式叫做“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”,而 ES6 的模块不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入。

eg:

1
2
//ES6 模块
import { stat, exists, readFile } from 'fs';

ES6 这种方式实质上是从 fs 模块中加载三个方法,其他方法不加载也就是不会加载 ES6 模块本身,因为它不是个对象,这种方法叫做“编译时加载”或者“静态加载”,即 ES6 可以在编译时就完成了模块加载,效率比 CommonJS 要。
注意一点: ES6 的模块自动采用严格模式,即便你没有在头部加上 “use strict”。

严格模式主要有以下限制。

1. 变量必须声明后再使用
2. 函数的参数不能有同名属性,否则报错
3. 不能使用with语句
4. 不能对只读属性赋值,否则报错
5. 不能使用前缀0表示八进制数,否则报错
6. 不能删除不可删除的属性,否则报错
7. 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
8. eval不会在它的外层作用域引入变量
9. eval和arguments不能被重新赋值
10. arguments不会自动反映函数参数的变化
11. 不能使用arguments.callee
12. 不能使用arguments.caller
13. 禁止this指向全局对象
14. 不能使用fn.caller和fn.arguments获取函数调用的堆栈
15. 增加了保留字(比如protected、static和interface)