You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
(function(root,factory){if(typeofdefine==='function'&&define.amd){// AMDdefine(['myModule','myOtherModule'],factory);}elseif(typeofexports==='object'){// CommonJSmodule.exports=factory(require('myModule'),require('myOtherModule'));}else{// Browser globals (Note: root is window)root.returnExports=factory(root.myModule,root.myOtherModule);}}(this,function(myModule,myOtherModule){// MethodsfunctionnotHelloOrGoodbye(){};// A private methodfunctionhello(){};// A public method because it's returned (see below)functiongoodbye(){};// A public method because it's returned (see below)// Exposed public methodsreturn{hello: hello,goodbye: goodbye}}));
js模块化编程不完全指北(Commonjs、AMD、CMD、ES6 modules)
称职的作家会把他的书分章节和段落;好的程序员会把他的代码分成模块。
模块化可以使你的代码低耦合,功能模块直接不相互影响。对可维护性、命名空间、可复用性上起了极大的作用。
一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。模块开发需要遵循一定的规范,否则就都乱套了。
一、模块模式(Module模式)
早年间,JS 还只是委身于 HTML
<script>
标签中的内联代码;顶不济也就是被封装到专门的脚本文件中调用,也还是得与其他脚本共享一个全局作用域。这样很容易会造成某个脚本中的变量可能会在无意之间被全局中或者其他脚本中的变量覆盖(全局变量污染)。模块模式一般用来模拟类的概念,这样我们就能把公有和私有方法还有变量存储在一个对象中。这样我们就能在公开调用API的同时,仍然在一个闭包范围内封装私有变量和方法。
方式:匿名闭包函数、全局引入、对象接口等。
二、CommonJS & AMD & UMD & CMD
模块模式(Module模式)都是通过使用单个全局变量来把所有的代码包含在一个函数内,由此来创建私有的命名空间和闭包作用域。这样也会存在一个问题,模块和模块之间也可能会出现命名冲突,那么存在不使用全局作用域来实现的模块方法么?哈那就是社区中广受欢迎的 CommonJS 和 AMD规范了。
1. CommonJS(同步加载模块)
CommonJS就是一个JavaScript模块化的规范,node.js的模块系统,就是参照CommonJS规范实现的。该规范最初是用在服务器端的node的,前端的webpack也是对CommonJS原生支持的。
根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为global对象的属性。
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
CommonJS的核心思想就是通过 require 方法来同步加载所要依赖的其他模块,然后通过
exports
或者module.exports
来导出需要暴露的接口。CommonJS定义的模块分为:{模块引用(require)}、 {模块定义(exports)} 、{模块标识(module)}。
require()用来引入外部模块;exports对象用于导出当前模块的方法或变量,唯一的导出口;module对象就代表模块本身。
浏览器不兼容CommonJS的根本原因:在于缺少四个Node.js环境的变量(module、exports、require、global)。只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。例如:
在CommonJS中,有一个全局性方法require(),用于加载模块。正是由于CommonJS 使用的require方式的推动,才有了后面的AMD、CMD 也采用的require方式来引用模块的风格。
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
这样,在对外输出模块接口时,可以向exports对象添加方法。
每个js文件一创建,都有一个
var exports = module.exports = {}
, 使exports和module.exports都指向一个空对象。module.exports和exports所指向的内存地址相同注意,不能直接将exports变量指向一个值(或者指向一个函数,注意不是添加一个方法),因为这样等于切断了exports与module.exports的联系。例如:
当然如果觉得,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。
下面举例说明:
(1):
以上代码可以看出,b模块export的count变量,是一个复制行为。在plusCount方法调用之后,a模块中的count不受影响。同时,可以在b模块中更改a模块中的值(基本类型进行的是复制操作,所以不会影响另一个模块)。如果希望能够同步代码,可以export出去一个getter。
对于复杂数据类型,属于浅拷贝。若两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
类似于:
下面的代码在a模块修改count的值为3,此时在b模块的值也为3。这时修改的便是用一个引用的原因了.
(2): 当使用require命令加载某个模块时,就会运行整个模块的代码。(不是按需加载的)。
(3):当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。
(4):循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
同一个例子可说明:
模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本。
修改main.js,再次加载a.js和b.js。
上面代码中,第二次加载a.js和b.js时,会直接从缓存读取exports属性,所以a.js和b.js内部的console.log语句都不会执行了。
参考文章
CommonJS模块和ES6模块的区别
JS中的值是按值传递,还是按引用传递呢?
2. AMD规范
默认情况下,浏览器是同步加载 JavaScript 脚本。如果脚本体积很大
或者有异步行为(异步行为会等到空闲的时候执行),下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。即对于浏览器,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间这显然是很不好的体验,所以浏览器允许脚本异步加载。也就是说,浏览器端的模块,不能采用”同步加载”(synchronous),只能采用”异步加载”(asynchronous)。这就是AMD规范诞生的背景。这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成。
早年HTML 网页中,浏览器通过
<script>
标签加载 JavaScript 脚本。<script>
标签打开defer或async属性,脚本就会异步加载。CommonJS规范加载模块是同步的,使得CommonJS规范不适用于浏览器环境。
AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD规范使用define方法定义模块。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
优点:可以看出,AMD规范适合在浏览器环境中异步加载模块。并且可以并行加载多个模块。除了异步加载以外,AMD的另一个优点是你可以在模块里使用对象、函数、构造函数、字符串、JSON或者别的数据类型,而CommonJS只支持对象。
缺点:提高了开发成本,并且不能按需加载,而是必须提前加载所有的依赖。
3. UMD规范
在一些同时需要 AMD 和 CommonJS 功能的项目中,你需要使用另一种规范:Universal Module Definition(通用模块定义规范)。
UMD创造了一种同时使用两种规范的方法,并且也支持全局变量定义。所以UMD的模块可以同时在客户端和服务端使用。下面是一个解释其功能的例子:
4. CMD
CMD规范(Common Module Definition)是阿里的玉伯提出来的,实现js库为sea.js。 它和requirejs非常类似,即一个js文件就是一个模块,但是CMD的加载方式更加优秀,是通过按需加载的方式,而不是必须在模块开始就加载所有的依赖。
CMD与AMD一样,也是采用特定的define()函数来定义,用require方式来引用模块
AMD和CMD的区别: 二者皆为异步加载模块,AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。
三、ES6 module(目前浏览器无法识别,需要使用 babel 转换为 commonjs规范)
你可能注意到了,上述的这几种方法都不是JS原生支持的。要么是通过模块模式来模拟,要么是使用CommonJS或AMD。
幸运的是在JS的最新规范ECMAScript 6 (ES6)中,引入了模块功能。
ES6 的模块功能汲取了CommonJS 和 AMD 的优点,拥有简洁的语法并支持异步加载,并且还有其他诸多更好的支持(ep: 导入是实时只读的)。
ES6标准发布后,module成为标准,标准使用是以 export 指令导出接口,以 import 引入模块,但是在我们一贯的 node 模块中,我们依然采用的是 CommonJS 规范(commonjs规范最初就是用于node服务端的),使用require 引入模块,使用 module.exports 导出接口。
ES6 module 较之于前几个方案更为强大,也是我们所推崇的,但是由于ES6目前无法在浏览器中执行,所以,我们只能通过babel将不被支持的import编译为当前受到广泛支持的 require。
1. export 导出模块
export语法声明用于导出函数、对象、指定文件(或模块)的原始值。
export有两种模块导出方式:命名式导出(名称导出)和默认导出(定义式导出),命名式导出每个模块可以多个,而默认导出每个模块仅一个。
export在导出接口的时候,必须与模块内部的变量具有一一对应的关系。直接导出1没有任何意义,也不可能在import的时候有一个变量与之对应。export a虽然看上去成立,但是a的值是一个数字,根本无法完成解构,因此必须写成export {a}的形式。即使a被赋值为一个function,也是不允许的。而且,大部分风格都建议,模块中最好在末尾用一个export导出所有的接口,例如:
2. import 引入模块
import语法声明用于从已导出的模块、脚本中导入函数、对象、指定文件(或模块)的原始值。
import模块导入与export模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和默认导入(定义式导入)。
require的使用非常简单,它相当于module.exports的传送门,module.exports后面的内容是什么,require的结果就是什么,对象、数字、字符串、函数……再把require的结果赋值给某个变量,相当于把require和module.exports进行平行空间的位置重叠。
而且require理论上可以运用在代码的任何地方,甚至不需要赋值给某个变量之后再使用,比如:
你在使用时,完全可以忽略模块化这个概念来使用require,仅仅把它当做一个node内置的全局函数,它的参数甚至可以是表达式:
但是import则不同,它是编译时的(require是运行时的),它必须放在文件开头,而且使用格式也是确定的,不容置疑。它不会将整个模块运行后赋值给某个变量,而是只选择import的接口进行编译,这样在性能上比require好很多。
从理解上,require是赋值过程,import是解构过程,当然,require也可以将结果解构赋值给一组变量,但是import在遇到default时,和require则完全不同:
var $ = require('jquery');
和import $ from 'jquery'
是完全不同的两种概念。参考文章
彻底搞清楚javascript中的require、import和export
Node中没搞明白require和import,你会被坑的很惨
四、ES6 module 与 CommonJS 模块的差异
写法上的差异:
两个重大差异:
举例说明:
上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量(export 出去的变量)绑定其所在的模块。
下面代码中,m1.js的变量foo,在刚加载时等于bar,过了 500 毫秒,又变为等于xql。
让我们看看,m2.js能否正确读取这个变化。
上面代码表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。
由于 ES6 输入的模块变量,只是一个“符号连接”(不像 commonjs 是一个值的拷贝/副本, 相当于把被引入的模块代码全部复制了一份放过来),所以这个变量是只读的,对它进行重新赋值会报错。
还有,export通过接口,输出的是同一个值。不同的脚本加载这个接口,引用的都是同一个实例(编译时就 export 输出接口了,所以在引入时拿到的是同一个值)。
所以,在es6 modules 中,引入模块时,尽量不要改变模块内的变量。
首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。
解决这个问题的方法,就是让b.mjs运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决。
这是因为函数具有提升作用,在执行import {bar} from './b'时,函数foo就已经有定义了,所以b.mjs加载的时候不会报错。这也意味着,如果把函数foo改写成函数表达式,也会报错。
References
深入理解JavaScript系列(3):全面解析Module模式
CommonJS规范
Module 的加载实现
CommonJS模块和ES6模块的区别
模块化利器: 一篇文章掌握RequireJS常用知识
The text was updated successfully, but these errors were encountered: