JavaScript - 模块化
关于JavaScript中模块化
的一些总结。
在发开大型软件项目时,一般需要通过模块化
来管理代码。将一段有逻辑联系的代码封装成模块
,其内部数据与实现是私有的,将一些公用接口暴露出去,与其他模块进行通信,最后组织成程序。
模块化的作用:
- 避免命名冲突
- 按需加载
- 提高复用程度
- 提高可维护性
软件工程提倡高内聚,低耦合。
目前流行的JS模块化规范有CommonJS
、AMD
、CMD
和ES6模块
。通常将一个JS文件作为一个模块,向外暴露特定的变量和函数。
CommonJS
Node
是CommonJS
规范的主要实践者,通过module
、exports
、require
等关键词实现模块加载
和模块定义
。
在服务端一般模块文件都存在本地,读取速度比较快,所以使用同步加载方式一般没问题。
CommonJS
以同步方式加载模块。
模块引用
使用require()
方法引入模块,传入的变量为模块标识符。
在Node中有三种模块类型,核心模块
、文件模块
、自定义模块
。
- 核心模块: Node内置的一些模块,例如
http
、fs
、path
等,模块标识符为模块名称。在Node的源码编译过程中已经将这些模块编译为二进制代码,所以加载过程很快。 - 文件模块: 一般为用户自己定义的一些模块,一个文件就是一个模块,模块标识符为文件路径。由于指明了文件加载路径,所以加载速度较快。
- 自定义模块: 用户使用
npm
等包管理工具安装的模块,一般保存在node_moudles
目录下,模块标识符为模块名称。由于需要按照npm
的加载规则匹配模块,所以加载过程较慢。
|
模块定义
对于文件模块,我们需要自己定义,一个文件就是一个模块。在CommonJS规范中exports
对象是模块的唯一出口,定义导出的变量或方法。
|
但是Node并不是完全按照CommonJS规范实现的,而是根据自身需要做了一些取舍,所以表现出一些特性。在Node中,我们更推荐使用module.exports
来定义模块。
|
module.exports 和 exports
在Node中,一个模块(文件)就是一个闭包,通过闭包机制实现了命名空间。
|
每执行一个文件,就会自动创建一个module
对象,而module.exports
是其中的一个属性。所以module.exports
才是真正的模块对象,exports
只是对它的一个引用。
因此,使用module.exports
可以对该变量重新赋值,而使用exports
不能进行重新赋值,只能对exports
下的属性进行赋值,如exports.add = add
。
ES6模块
ES6
标准在语法层面上实现了模块化,通过使用import
和export
实现模块加载
和模块定义
。由于是标准规范,使用在客户端和服务端都可以使用,一般是配合webpack
等打包工具进行管理,因为现阶段ES6
还需通过babel
进行编码。
不同于其他三种模块化方式,ES6模块
是在编译过程中加载模块,而不是动态地引入一个对象。
模块定义
使用export
命令导出模块,也可以使用export default
指定默认输出模块。
|
模块引用
使用import
命令导入模块,可以使用解构赋值
的方式导入。
|
CommonJS 和 ES6模块
摘自 - 前端模块化:CommonJS,AMD,CMD,ES6
CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用
CommonJS
模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6模块
的运行机制与CommonJS
不一样。JS引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6
的import
有点像Unix
系统的符号连接
,原始值变了,import
加载的值也会跟着变。因此,ES6模块
是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
CommonJS模块是运行时加载,ES6模块是编译时输出接口
- 运行时加载:
CommonJS
模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。 - 编译时加载:
ES6模块
不是对象,而是通过export
命令显式指定输出的代码,import
时采用静态命令的形式。即在import
时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
CommonJS
加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而ES6模块
不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
AMD
require.js
是AMD
规范的主要实践者,通过define
定义模块、require
引入模块。
在客户端,一般是远程加载模块,受网络限制,使用异步方式更合理。
AMD
以异步方式加载模块,推崇依赖前置,提前执行,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
模块引用
首先需要在HTML
中使用<script>
引入require.js
和主模块。
|
使用require.config()
引入模块文件,可以设置路径前缀、设置模块名称。再使用require()
使用模块,如下方式设置别名。
|
模块定义
使用define()
方法定义一个模块,模块内容放在回调函数
中,并通过return
抛出。
|
CMD
sea.js
是CMD
规范的主要实践者,CMD
吸收了CommonJS
和AMD
。
CMD
以异步方式加载模块,推崇依赖就近,延迟执行,在代码中引入模块并运行。
模块引用
首先需要在HTML
中使用<script>
引入sea.js
和主模块。
|
使用define()
创建作用域,在回调函数
(传入参数reuqire)中通过require()
引入模块。
|
模块定义
|
命名空间
JS的全局变量其实是定义在一个window对象下的,例如var history = 1;
其实是var window.history = 1;
。然而,window对象又有许多的内置属性,如果我们自定义的属性刚好和内置属性重名的话,就会起冲突。
因此,为了避免变量冲突,就有必要引入命名空间
来隔离变量作用域,其实是用到闭包
和自触发函数
来模拟的。许多模块化
的库都是基于这一原理。
自触发函数
先来看看自触发函数
(IIFE),在函数定义后立即触发。因为立即执行,后续一般不会再调用,所以可以使用匿名函数。
|
命名空间
结合闭包
和立即执行函数
,我们就可以很好的封装一个模块了。闭包
用来限定作用域(命名空间),立即执行函数
避免全局变量名字冲突。
jQuery
就是用于以上两种机制,暴露出了一个$
,来操作内部方法。
以下例子,封住了user
模块,解决变量冲突。
|
此外,ES6中的let
和const
可以产生一个块级作用域,其实也是用到了该原理。用babel
编译后是如下ES5代码,是不是一目了然。
|
|
命名空间模式也存在许多不足,一般在ES5
年代比较多见。现在一般都是用模块化
的方式直接引入文件模块了。
参考文献
- 本文作者:zhaoo
- 本文链接:https://www.izhaoo.com/2020/04/12/js-module/index.html
- 版权声明:本博客所有文章均采用 BY-NC-SA 许可协议,转载请注明出处!