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