Modules

模块话,模块化说了那么多次,以为了解个大概就行了,然而在一次面试经历中被问到AMD和ES Module,所以特此来一篇,汇总一下吸收的模块方面的内容。

又是一篇搬运文章,谁让我放荡不羁爱打野呢。作者博客地址Preethi Kasireddy

为什么模块很重要?

如果没有模块,你能想象在复杂场景下你得js代码是个什么鬼模样吗?模块解决了名称空间和可维护性等变得越来越难以处理的问题。

好的模块是高度独立的,具有独特的功能,可以根据需要对它们进行改组,删除或添加,而不会破坏整个系统。

优势:

1)可维护性:根据定义,模块是独立的。精心设计的模块旨在尽可能减少对代码库各部分的依赖,从而使其能够独立增长和改进。当模块与其他代码解耦时,更新单个模块要容易得多。

2)命名空间:在JavaScript中,顶级函数范围之外的变量是全局变量(意味着每个人都可以访问它们)。因此,普遍存在“命名空间污染”,其中完全不相关的代码共享全局变量。

在不相关的代码之间共享全局变量在开发中是一个很大的禁忌。模块允许我们通过为变量创建私有空间来避免名称空间污染。

3)可重用性:抽取通用部分,哪里需要就拿去,不用重复写,当然也对应的第一点,当有修改时只需要该一份。

早期的时候为了达到“模块模式”,也有很多方式,不过我看了下基本上都是基于匿名闭包的基础上而来的。

这些方式有一个共同点:使用单个全局变量将其代码包装在函数中,从而使用闭包作用域为其自身创建私有名称空间,同时自定义公开哪些方法、变量

大家可以看下jQuery的源码,就是这样的实现方式,如下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var myGradesCalculate = (function () {

// Keep this variable private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];

var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);

return'Your average grade is ' + total / myGrades.length + '.';
};

var failing = function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});

return 'You failed ' + failingGrades.length + ' times.';
};

// Explicitly reveal public pointers to the private functions
// that we want to reveal publicly

return {
average: average,
failing: failing
}
})();

myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

如您所见,这种方法使我们可以决定将哪些变量/方法设为私有(例如*myGrades*),以及通过将它们放在return语句中(例如*average**failing***)来公开哪些变量/方法。

所以大家可以看出,早期关于模块的写法,尽管每种方法都以其自己的方式有效,但它们也有缺点:

  1. 大家各自发挥的,这样有个最大的问题就是到最后就是乱的,就像为什么需要TC39一样,让大家有一套标准,才能从而写出更友好通用的模块。
  2. 依赖管理是个问题,需要我们开发人员自己管理依赖,记得刚开始用jQuery这种第三方库的时候,如果没注意到顺序就会报错,所以想象一下如果引用较多时,管理依赖关系并正确解决这些问题会让人头疼。
  3. 全局作用域被污染,上述的方式创建的变量(比如myGradesCalculate)都在全局范围内,所以该全局范围内的代码的每个部分都可以更改该变量。恶意代码可以有意更改该变量,以使您的代码执行您不希望这样做的事情,或者非恶意代码可能会无意间破坏了您的变量。

所以基于上述原因,这个时候我们就需要一套规范了来解决这些个问题:我们能否设计一种无需遍历全局范围即可请求模块接口的方法

规范

现在比较通行得规范有两种:CommonJSAMD

CommonJS

CommonJS是一个旨在定义一系列规范以帮助开发服务器端JavaScript应用程序的项目。CommonJS团队尝试解决的领域之一就是模块,负责设计和实现用于声明模块的JavaScript API。我听说CommonJS,最早是在15年写Node应用的时候接触的,Node.js最开始就是遵循这套规范弄得模块化,但是据说后来不用该规范了。

一个CommonJS的模块本质上是一种可重复使用的一段JavaScript代码其中出口特定对象,使它们可用于其他模块需要在他们的计划。

使用CommonJS,每个JavaScript文件都将模块存储在其自己的唯一模块上下文中(就像将其包装在闭包中一样)。在此范围内,我们使用module.exports对象公开模块,并要求将其导入。

当您定义CommonJS模块时,它可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
function myModule() {
this.hello = function() {
return 'hello!';
}

this.goodbye = function() {
return 'goodbye!';
}
}

module.exports = myModule;

我们使用特殊对象模块,并将我们函数的引用放入module.exports中。这使CommonJS模块系统知道我们要公开的内容,以便其他文件可以使用它。

然后,当某人想要使用myModule时,他们可以在其文件中要求它,如下所示:

1
2
3
4
5
var myModule = require('myModule');

var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'

与我们之前讨论的模块模式相比,这种方法有两个明显的好处:

  1. 避免全局命名空间污染
  2. 明确我们的依赖关系

要注意的另一件事是,CommonJS采用服务器优先的方法并同步加载模块。这很重要,因为如果我们有我们需要的其他三个模块需要,它就会加载它们一个接一个。

现在,它可以在服务器上很好地工作,但是不幸的是,这使得为浏览器编写JavaScript时更难使用,因为服务器端通常是从磁盘读取,而浏览器需要网络请求,所以只要加载模块的脚本一直在运行(JavaScript线程将停止直到代码被加载),它就会阻止浏览器运行其他任何东西,直到加载完成。

AMD

从上面我们知道CommonJS是同步的,所以很显然不适用浏览器端,那我们就需要异步模块定义的规范,即AMD

使用AMD加载模块如下所示:

1
2
3
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});

这里发生的是,define函数将每个模块依赖项的数组作为第一个参数。这些依赖项在后台加载(以非阻塞方式),并且一旦加载了define,便调用回调函数。

接下来,回调函数将加载的依赖项作为参数(在本例中为myModule*myOtherModule),以允许函数使用这些依赖项。最后,还必须使用**define***关键字定义依赖项本身。

例如,***myModule***可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
define([], function() {

return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});

与CommonJS不同,AMD采用了浏览器优先的方法以及异步行为来完成工作。

除了异步之外,AMD的另一个好处是您的模块可以是对象,函数,构造函数,字符串,JSON和许多其他类型,而CommonJS仅支持将对象作为模块。

AMD与CommonJS相比,其提供的io,文件系统和其他面向服务器的功能不兼容。

UMD

对于需要同时支持AMD和CommonJS功能的项目,还有另一种格式:通用模块定义(UMD)。

UMD本质上创建了一种使用这两种方法之一的方式,同时还支持全局变量定义。结果,UMD模块能够在客户端和服务器上工作。

以下是UMD如何开展业务的快速体验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['myModule', 'myOtherModule'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('myModule'), require('myOtherModule'));
} else {
// Browser globals (Note: root is window)
root.returnExports = factory(root.myModule, root.myOtherModule);
}
}(this, function (myModule, myOtherModule) {
// Methods
function notHelloOrGoodbye(){}; // A private method
function hello(){}; // A public method because it's returned (see below)
function goodbye(){}; // A public method because it's returned (see below)

// Exposed public methods
return {
hello: hello,
goodbye: goodbye
}
}));

ES Module

是我做前端开始听的最多的了,当然也因为无时无刻都在用它。

上面咱们所说的,都不是JavaScript固有的。不过幸运的是,TC39(定义ECMAScript语法和语义的标准机构)引入了ECMAScript 6(ES6)内置模块。

ES6提供了多种导入和导出模块的可能性,其他人则做了很好的解释-以下是其中的一些资源:

与CommonJS或AMD相比,ES6模块最大的优点是它能够提供两全其美的优势:紧凑和声明性语法以及异步加载,以及诸如更好地支持依赖项等附加优点。

ES6模块最让人兴奋的应该是导入是导出的实时只读视图,即是只读引用,不过却可以改写属性。所以你猜到了当你模块里的值属性发生变化时,导入的地方获取的值是一样的。ES Module具体的后面还有一篇文档单讲,不然这文章就太长了。

最后我们对比一下两种方式吧

ES Module与CommonJS:

  • CommonJS规范通常适用于Node这类服务器端的
  • CommonJS模块是对象,是运行时加载,运行时才把模块挂载在exports之上(加载整个模块的所有),加载模块其实就是查找对象属性。
  • ES Module不是对象,是使用export显示指定输出(函数、对象、变量等),再通过import导入。为编译时加载,编译时遇到import就会生成一个只读引用。等到运行时就会根据此引用去被加载的模块取值。所以不会加载模块所有方法,仅取所需。
  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

说明:

全局范围(作用域)

  1. 写在script标签中的JS代码,都在全局作用域 。
  2. 全局作用域在页面打开时创建,在页面关闭时销毁。
  3. 在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用 。
  4. 全局作用域中,创建变量都会作为window对象的属性保存
  5. 创建的函数都会作为window对象的方法保存
  6. 全局作用域中的变量都是全局变量,在页面的任何部分都可以访问的到并可以修改它

来源: