js模块化(ESModule与CommonJS)

写在前面的

模块化开发方式可以提高代码复用率,方便进行代码管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前我们开发中主要用到的是 CommonJS 和 ES Module。

模块化带来的好处:

  • 避免命名冲突
  • 更好的分离
  • 更高复用性
  • 更高可维护性

不知道大家有没有遇到过 export、exports、export default、module.exports 傻傻分不清的情况?

1、CommonJS

Node.js是commonJS规范的主要实践者。Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

CommonJS 使用module.exports、exports导出;使用require导入

导出(module和exports)

在node环境直接打印module对象js模块化(ESModule与CommonJS)_第1张图片
每个模块(js文件)内部都有一个module对象,代表当前模块,它有以下属性

  • id: 模块的识别符,通常是带有绝对路径的模块文件名。
  • filename: 文件名
  • parent:调用该模块的模块(父模块)
  • children: 该模块要用到的其他模块(子模块)
  • loaded: 表示模块是否已经完成加载
  • exports: 表示模块对外输出的值
    js模块化(ESModule与CommonJS)_第2张图片
    为了方便,Node为每个模块提供一个exports变量,指向module.exports。
    js模块化(ESModule与CommonJS)_第3张图片
    需要注意的是,不能将exports变量指向一个值,因为那样就等同于切断了exports和module.exports的联系;同样如果使用module.exports = {}的形式赋值,也会丢失exports导出的内容。看下面这种情况
    js模块化(ESModule与CommonJS)_第4张图片
    exports.a和module.exports.b都是在原有的module.exports对象上增加a、b两个属性,但module.exports = { c: 30 },直接改变了module.exports所指向的内存地址。
    如果你觉得,exports和module.exports之前的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。
    总结:CommonJS导出的是module.exports对象

导入(require命令)

require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象,如果没有发现指定模块,会报错。commonjs是在某个模块具体加载的时候(运行时)才去确定和加载这个模块,所以require使用比较灵活。
js模块化(ESModule与CommonJS)_第5张图片
commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

2、ES Module

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

ES Module采用export、export default导出,使用import关键字导入

导出(export和default)

export导出变量的方式有以下几种写法
js模块化(ESModule与CommonJS)_第6张图片
需要注意的是,export命令规定是对外的接口,必须与模块内部的变量建立一一对应的关系;所以下面这些写法是错误❌的
js模块化(ESModule与CommonJS)_第7张图片
使用import的时候,用户需要知道所加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-hello.js 
export default function() {
	console.log('hello world')
}

// import-hello.js
import helloWorld from './export-hello'
helloWorld()

本质上,export default 就是输出一个叫做default的变量,所以使用export default需要注意以下几点:

  • 一个模块只能用一次export default。显然,一个模块只能有一个默认输出(default对象)
  • export default 后面不能跟变量声明语句,export default a就相当于把a赋值给default变量,然后导出
    js模块化(ESModule与CommonJS)_第8张图片
    export 和 export default 在模块中是可以共同使用的
// es.js
export const a = 10
const b = 20
export default b

// index.js
import b, {a} from './es'

总结: ES Module中export导出的是接口

导入(import)

使用export命令定义了模块的对外接口以后,其他js文件可以通过import命令加载这个模块

// 导入export导出的变量
import { a, b, c } from './letter.js'

// 导入export default导出的默认输出
import anyName from './default.js'

使用 export 导出的变量,导入时需要加上{}, 且命名要和导出 声明中的保持一致
使用 export default 导出的变量,导入是不用加{}, 并且命名可以任意
由于esm是在静态编译期间就决定所有模块的依赖情况,所以不能使用表达式和变量,因为这些只有在运行时才能得到结果的语法结构
js模块化(ESModule与CommonJS)_第9张图片
上面代码中,JavaScript引擎处理import语句是在编译阶段,是不会去分析或执行’f’ + ‘oo’、module = 'my_module’以及if语句的,所以会报语法错误,而不是执行错误。

ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析.
所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。

因为import在静态解析阶段执行,它是一个模块之中最早执行的,所以import只能作为模块顶层的语句出现。

export与import复合写法

import { a, b } from './letter'
export { a, b }

// 一行语句实现
export { a, b } from './letter'

// 默认导出
export { default } from './default'

// 默认接口改为具名接口
export { default as es6 } from './moduleA'

// 具名接口改为默认接口
export { es6 as default } from './moduleB'

dynamic-import

es2020提案引入import()函数,支持动态加载模块。import命令能够接受什么样的参数,import()函数就能接受什么参数,两者区别在于
import()是动态加载的,并且会返回一个promise对象。
通常我们在写路由的时候会用到import()函数
js模块化(ESModule与CommonJS)_第10张图片

// demo03/es
export const a = 10
export const b = 20
export default function sayHello() {
  console.log('hello')
}

// main.js
const res = import('./demo03/es').then(module => {
  console.warn('module is', module)
}).catch(err => {
  console.warn(err)
})
console.warn('import()的返回值', res)

当模块加载成功后,这个模块会作为一个对象,当作then方法的参数,所以可以还通过解构赋值的语法,获取输出的接口

const res = import('./demo03/es').then(({a,b,default}) => {
  console.warn('a', a)
  console.warn('b', b)
  console.warn('default', default)
}).catch(err => {
  console.warn(err)
})
console.warn('import()的返回值', res)

import()函数可以用在任何地方,它和CommonJS的require方法类似,是运行时执行,所以是放在方法或者表达式里面的

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

3、CommonJS和ES Module区别

CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

运行时加载:commonjs模块就是对象;即在输入时先加载整个模块,生成一个对象,然后再从这个对象上读取属性和方法。也这样理解:Commonjs加载的是一个对象(module.exports属性),该对象只有在脚本运行时才能生成。
js模块化(ESModule与CommonJS)_第11张图片
编译时加载(静态加载):ES6模块不是对象,而是通过export命令显式指定输入的代码,Import时可以指定加载某个导出的值,而不是加载整个模块。这种加载方式的好处在于加载效率高,能进一步拓展JavaScript的语法,比如引入宏和类型校验这些只能靠静态分析实现的功能。包括tree shaking的实现,也是依赖es6 module的静态加载。

commonjs是导出值的拷贝,es6 module是导出值的引用

commonjs导出值拷贝:commonjs的一个模块就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象(拷贝)。
js模块化(ESModule与CommonJS)_第12张图片
以后需要用到这个模块的时候,就会在exports属性上面取值,即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值,也就是说commonjs模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载就返回第一次运行的结果。

es导出值引用:es模块遇到模块加载命令import,就会生成一个只读引用,等到脚本执行时,再根据这个只读引用,到被加载的那个模块里面去取值,原始值变了,import加载的值也会跟着变。因此,es6模块是动态引用,不会有缓存值,模块里的变量绑定其所在的模块。
js模块化(ESModule与CommonJS)_第13张图片
参考链接:
阮一峰ES6入门
前端模块化详解
聊聊CommonJS与ES5 Module的使用与区别

你可能感兴趣的:(JavaScript,es6,前端,javascript)