title: Modules author: Gamehu tags:
{% asset_img a.png Photo by Sharon McCutcheon from Pexels %}
模块话,模块化说了那么多次,以为了解个大概就行了,然而在一次面试经历中被问到AMD和ES Module,所以特此来一篇,汇总一下吸收的模块方面的内容。
又是一篇搬运文章,谁让我放荡不羁爱打野呢。作者博客地址Preethi Kasireddy
如果没有模块,你能想象在复杂场景下你得js代码是个什么鬼模样吗?模块解决了名称空间和可维护性等变得越来越难以处理的问题。
好的模块是高度独立的,具有独特的功能,可以根据需要对它们进行改组,删除或添加,而不会破坏整个系统。
1)可维护性:根据定义,模块是独立的。精心设计的模块旨在尽可能减少对代码库各部分的依赖,从而使其能够独立增长和改进。当模块与其他代码解耦时,更新单个模块要容易得多。
2)命名空间:在JavaScript中,顶级函数范围之外的变量是全局变量(意味着每个人都可以访问它们)。因此,普遍存在“命名空间污染”,其中完全不相关的代码共享全局变量。
在不相关的代码之间共享全局变量在开发中是一个很大的禁忌。模块允许我们通过为变量创建私有空间来避免名称空间污染。
3)可重用性:抽取通用部分,哪里需要就拿去,不用重复写,当然也对应的第一点,当有修改时只需要该一份。
早期的时候为了达到“模块模式”,也有很多方式,不过我看了下基本上都是基于匿名闭包的基础上而来的。
这些方式有一个共同点:使用单个全局变量将其代码包装在函数中,从而使用闭包作用域为其自身创建私有名称空间,同时自定义公开哪些方法、变量。
大家可以看下jQuery的源码,就是这样的实现方式,如下面的代码。
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*)来公开哪些变量/方法。
所以大家可以看出,早期关于模块的写法,尽管每种方法都以其自己的方式有效,但它们也有缺点:
所以基于上述原因,这个时候我们就需要一套规范了来解决这些个问题:我们能否设计一种无需遍历全局范围即可请求模块接口的方法
CommonJS是一个旨在定义一系列规范以帮助开发服务器端JavaScript应用程序的项目。CommonJS团队尝试解决的领域之一就是模块,负责设计和实现用于声明模块的JavaScript API。我听说CommonJS,最早是在15年写Node应用的时候接触的,Node.js最开始就是遵循这套规范弄得模块化,但是据说后来不用该规范了。
一个CommonJS的模块本质上是一种可重复使用的一段JavaScript代码其中出口特定对象,使它们可用于其他模块需要在他们的计划。
使用CommonJS,每个JavaScript文件都将模块存储在其自己的唯一模块上下文中(就像将其包装在闭包中一样)。在此范围内,我们使用*module.exports*对象公开模块,并要求将其导入。
当您定义CommonJS模块时,它可能看起来像这样:
function myModule() {
this.hello = function() {
return 'hello!';
}
this.goodbye = function() {
return 'goodbye!';
}
}
module.exports = myModule;
我们使用特殊对象模块,并将我们函数的引用放入*module.exports中*。这使CommonJS模块系统知道我们要公开的内容,以便其他文件可以使用它。
然后,当某人想要使用*myModule时*,他们可以在其文件中要求它,如下所示:
var myModule = require('myModule');
var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'
与我们之前讨论的模块模式相比,这种方法有两个明显的好处:
要注意的另一件事是,CommonJS采用服务器优先的方法并同步加载模块。这很重要,因为如果我们有我们需要的其他三个模块*需要*,它就会加载它们一个接一个。
现在,它可以在服务器上很好地工作,但是不幸的是,这使得为浏览器编写JavaScript时更难使用,因为服务器端通常是从磁盘读取,而浏览器需要网络请求,所以只要加载模块的脚本一直在运行(JavaScript线程将停止直到代码被加载),它就会阻止浏览器运行其他任何东西,直到加载完成。
从上面我们知道CommonJS是同步的,所以很显然不适用浏览器端,那我们就需要异步模块定义的规范,即AMD。
使用AMD加载模块如下所示:
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});
这里发生的是,define函数将每个模块依赖项的数组作为第一个参数。这些依赖项在后台加载(以非阻塞方式),并且一旦加载了define,便调用回调函数。
接下来,回调函数将加载的依赖项作为参数(在本例中为*myModule*和myOtherModule),以允许函数使用这些依赖项。最后,还必须使用*define*关键字定义依赖项本身。
例如,*myModule*可能看起来像这样:
define([], function() {
return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});
与CommonJS不同,AMD采用了浏览器优先的方法以及异步行为来完成工作。
除了异步之外,AMD的另一个好处是您的模块可以是对象,函数,构造函数,字符串,JSON和许多其他类型,而CommonJS仅支持将对象作为模块。
AMD与CommonJS相比,其提供的io,文件系统和其他面向服务器的功能不兼容。
对于需要同时支持AMD和CommonJS功能的项目,还有另一种格式:通用模块定义(UMD)。
UMD本质上创建了一种使用这两种方法之一的方式,同时还支持全局变量定义。结果,UMD模块能够在客户端和服务器上工作。
以下是UMD如何开展业务的快速体验:
(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
}
}));
是我做前端开始听的最多的了,当然也因为无时无刻都在用它。
上面咱们所说的,都不是JavaScript固有的。不过幸运的是,TC39(定义ECMAScript语法和语义的标准机构)引入了ECMAScript 6(ES6)内置模块。
ES6提供了多种导入和导出模块的可能性,其他人则做了很好的解释-以下是其中的一些资源:
与CommonJS或AMD相比,ES6模块最大的优点是它能够提供两全其美的优势:紧凑和声明性语法以及异步加载,以及诸如更好地支持依赖项等附加优点。
ES6模块最让人兴奋的应该是导入是导出的*实时*只读视图,即是只读引用,不过却可以改写属性。所以你猜到了当你模块里的值属性发生变化时,导入的地方获取的值是一样的。ES Module具体的后面还有一篇文档单讲,不然这文章就太长了。
- 写在script标签中的JS代码,都在全局作用域 。
- 全局作用域在页面打开时创建,在页面关闭时销毁。
- 在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用 。
- 全局作用域中,创建变量都会作为window对象的属性保存
- 创建的函数都会作为window对象的方法保存
- 全局作用域中的变量都是全局变量,在页面的任何部分都可以访问的到并可以修改它
{% blockquote darlyn https://juejin.im/post/5b7d2f45e51d4538826f4c28 AMD , CMD, CommonJS,ES Module,UMD %}
{% endblockquote %}
{% blockquote Sebastian Peyrott https://auth0.com/blog/javascript-module-systems-showdown/ JavaScript Module Systems Showdown: CommonJS vs AMD vs ES2015 %}
{% endblockquote %}