模块机制诞生的背景
JavaScript语言自身的缺陷
说起JavaScript语言,是一个典型的从弱小到壮大的奋斗史。起初,它只是一个非常不起眼的语言,用来处理非常小众的问题。从设计之初,目标就是解决一些脚本语言的问题,因为需要处理的场景没那么复杂(最初是考虑用作表单校验和网页特效),且是仓促地被创造出来,所以它自身的各种缺陷和缺点也被各种编程人员广为诟病。
随着B/S架构的市场覆盖率越来越大,基于浏览器的系统日益增多,Javascript也被寄予了更多的期待,Javascript也从表单校验跃迁到应用开发的级别。在这个过程中,它大致经历了工具类库、组件库、前端框架、前端应用的变迁。
JavaScirpt不断被类聚和抽象,以便更好组织业务逻辑。但是,在这个过程中就发现,JavaScript缺少了一项功能:模块。对比其他编程语言,Java有类文件,Python有import机制,PHP有include和require。JavsScript啥都没有啊,即使可以通过<script>引入不同的js文件,但是终究是不太优雅的。
所以,在JavaScript中增加模块机制就是一个很重要的事情了。JavaScript社区就开始了JavaScript模块规范的制定,其中CommonJS规范的提出算是最为重要的里程碑,由Javascript社区于2009年提出,是包含模块、文件、IO、控制台在内的一系列标准。Node.js的实现中采用了CommonJS标准的一部分,并在其基础上进行了一些调整。
CommonJS规范
CommonJS中规定每个文件是一个模块。将一个JavaScript文件直接通过script标签插入页面中与封装成CommonJS模块最大的不同在于,前者的顶层作用域是全局作用域,在进行变量及函数声明时会污染全局环境,而后者会形成一个属于模块自身的作用域,所有的变量及函数只有自己能访问,对外是不可见的。
1. CommonJS的愿景
CommonJS的愿景——希望JavaScript能够在任何地方运行。在实际的应用中,JavaScript的表现能力取决于宿主环境中的API支持程度。
最初JavaScript只能运行在浏览器,服务端的JavaScript在Node.js出来之前基本没啥水花。而Node.js的成功也离不开CommonJS规范,Node借鉴CommonJS的Modules规范实现了一套非常易用的模块系统。
Node与浏览器以及W3C组织、CommonJS组织、ECMAScript之间的关系,共同构成了繁荣的生态系统。
2. CommonJS的模块规范
CommonJS的模块规范主要有模块引用、模块定义和模块标识三个部分。
2.1 模块引用
const events = require('events');
在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。
2.2 模块定义
CommonJS规范中,exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表的的是模块自身,而exports是modules的属性。
可以简单理解为,CommonJS在每个模块的首部默认添加了以下代码:
var module = {
exports:{},
}
var exports = module.exports;
在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式。
// test.js
exports.getRangeValue = generateRangeValue;
function generateRangeValue (min, max) {
// 最小值是min,所以加min
// 制定范围所以用随机数 * 区间数
return Math.floor(Math.random() * (max - min)) + min;
};
在另一个文件中,通过require()方法引入即可使用这个方法了
// index.js
const { getRangeValue } = require('./test')
console.log(getRangeValue(1,10))//2
在使用require导入一个模块时会有两种情况:
- 该模块未曾被加载过。这时会首先执行该模块,然后获取到该模块最终导出的内容。
- 该模块已经被加载过。这时模块的代码不会再次执行,而是直接获取该模块上一次导出的内容。
2.3 模块标识
模块标识其实就是传递给require()方法的参数,它必须符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径。它可以没有文件后缀名.js。
模块的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地链接上下游的依赖。
ES6模块
在CommonJS和AMD这样的模块化机制被广泛使用了之后,浏览器也坐不住了,感觉自己也应该支持模块化,2015年6月,由TC39委员会正式发布了ES6(ECMAScript 6.0),自此JavaScript语言才具备了模块这一特性。
浏览器原生的模块机制一般称为JavaScript模块,因为是在ES6标准实现的,所以大家一般称之为ES6module。
Javascript模块的设计和CommonJS差不多,重点都是为了实现导入导出。
1. export导出
// test.mjs
export function generateRangeValue(min, max) {
// 最小值是min,所以加min
// 制定范围所以用随机数 * 区间数
return Math.floor(Math.random() * (max - min)) + min;
}
export default function sayHi () {
console.log("Hello world!");
}
可以用export关键字定义导出的内容,export default可以定义这个模块的默认导出,就无须关心里面的内容其啥名了,可以在导入的地方对默认导出起别名。
2. import导入
// index.mjs
// 这里可以对默认导出起别名,也可以用as 对具名导出其别名
import hello, { generateRangeValue as getRangeValue} from './test.mjs';
hello();
console.log(getRangeValue(1, 10));
import关键词可以导入数据。
总结对比
1.CommonJS和ES6 module导出对象时,导出的都是值的引用(如果改变了导入的值会影响全局)
2.CommonJS的默认导出(module.exports)会覆盖其他的导出内容(可以理解为重新定义了module.exports对象),而ES6的默认导出和其他变量的导出可以共存
3.CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。ES6 Module可以做tree shaking ,而CommonJS不行。
4.CommonJS导入的是一个对象,而ES6Module支持直接导入变量,减少了引用层级,程序效率更高。
5.CommonJS导入的变量是值的副本,而ES6Module导入的变量是对原有值的动态映射,而且ES6Module规定不能对导入的变量进行修改。