Module语法
Module基本语法
- ES模块的设计思想:在编译时就能确定模块的依赖关系,以及输入输出变量(静态化)
- 编译时加载,若仅使用import导入特定的方法,则只加载该方法,其他方法不加载;
- 由于ES模块不是一个对象,导致无法引用模块本身
- 静态加载是静态分析成为可能,比如:宏、类型检验
- import和export会被静态分析,只能出现在模块的顶层作用域,毕竟在编译时,不会去执行形如if的代码
- 而CommonJS模块在运行时加载整个模块对象(只有在运行的时候才能知道加载的是哪个模块,可实现选择性加载),然后在该对象上读取需要的方法,无法做到静态优化
- ES模块内部自动采用严格模式,一些严格模式的特性有:【未完成】
- export命令:只有用该命令导出的变量,才能够被外界所读取
- export导出的接口,与值是动态绑定的,可实时取到模块内部的值;而其他模块(比如CommonJS模块输出的是值的缓存,不存在动态更新)
- export命令可以出现在模块内部的顶层任何位置,但是不能在模块内部的块作用域中,这样会报错(也无法做静态优化)
- 模块的默认导出:
export default xxx
,写法看代码- 本质上是导出一个叫做default的变量,且该变量在导入时可取任意名字
- 本质上是将xxx赋值给default变量,所以xxx必须是一个值(毕竟default = (var a = 1)是不合法的)
- import命令:加载模块
- import接受一个大括号,指定需要导入的模块方法,该方法必须与模块导出方法同名
- import输入的变量是只读的,即不能够修改该变量(该变量的内存地址不允许发生变量,变量属性还是可以修改的)
- import...from后面的路径,可为相对路径,也可为绝对路径,若仅有模块名,则必须有配置文件,让JS引擎知晓模块的位置
- import命令有提升效果,会提示到整个模块的头部,最先执行
- import是编译时执行的,所以在import语句中不允许出现表达式和变量(这些都是运行时执行)
- import可执行导入的整个模块,比如
import './a.js'
,此时a.js模块被执行,多次导入仅执行一次(import语句是单例模式) - 加载该模块的所有变量:
import * as all from './a.js'
,即用*号指定一个别名,这里也包括了default变量 - 模块的默认导入,即导入default变量:
import xxx from './a.js'
,其中xxx可取任意值 - 默认导入可与其他导入混合使用:
import xxx, { a, b } from './a.js'
- export和import混合使用:实际上当前模块未导入该接口,即刻对外转发导入的接口,但是当前模块不能使用该接口
- 模块的继承:说到底就是使用混合语法
- 跨常量模块(常量共享):也是使用混合语法
- import()函数:在ES2020中引入,支持动态加载模块
- import接受的参数,在import()函数中均能够接受
- 该函数返回Promise对象,为异步加载,且then的参数为整个模块对象,类似于加载所有变量(4.7)
- 该函数可用于任何地方
- 适用场景:
- 按需加载,比如监听事件后加载
- 条件加载,根据不同情况,加载不同模块
- 动态模块路径,内部可运行一个函数,函数的返回值为加载的路径
javascript
// export的写法:
// 写法一:声明语句前加上export
export var a = 1
export function b () {}
// 写法二:先声明,然后将变量(包括函数、类等等)赋给export中
var a = 1
function b () {}
export {
a,
b
}
// 写法三:可重命名,且可重命名多次
export {
a as aa,
a as aaa,
b as bb,
b as bbb
}
// 写法四:默认导出,不能直接导出变量声明,可用于class和function等正常的值
export default function () {}
// 即等于:
default = function () {}
// 所以:下面的是错误的
export default var a = 9
// 等于:显然是错误的
default = (var a = 9)
// 写法五:默认输出变量(比4靠谱)
var a = 1
export default a
// import写法:
// 写法一:使用大括号:
import { a, b } from './a.js'
// 写法二:使用大括号,并起别名:
import { a as aaa, b} from './a.js'
// 写法三:路径:纯模块名必须有配置文件
import { a, b } from 'a'
// 写法四:导入的整个模块被执行
import './a.js'
// 写法五:导入default
import defa from './a.js'
// 写法六:混合导入
import defa, { a, b } from './a.js'
// 导入导出混合写法:相当于中转站
// 写法一:
export { a, b } from './a.js'
// 写法二:接口改名
export { a as aaa , b as bbb } from './a.js'
// 写法三:中转所有
export * from './a.js'
// 写法四:中转所有并重命名(ES2020之后才有的)
export * as all from './a.js'
// 模块的接口改名(具名转默认)
export { a as default } from './a.js'
// 模块的接口改名(默认转具名)
export { default as a } from './a.js'
// import() 函数:
import('./a.js').then(all => {
console.log(all)
})
// 同时加载多个模块:
Promise.all([
import('./a.js'),
import('./b.js'),
import('./c.js')
]).then(([ma, mb, mc]) => {
console.log(aAll, bAll, cAll)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
注意事项
- 传统方法中,浏览器通过script标签加载脚本,默认为加载JavaScript(即可省略type属性)
- 默认情况下,JavaScript引擎是同步执行的,遇到script标签,首先下载脚本,然后执行脚本,最后才会继续执行其他内容,故而,异步方法有:
- 使用defer属性:整个页面在内存中正常渲染结束,才会执行该脚本(多个脚本按出现顺序加载)
- 使用async属性:脚本下载完成,引擎就会停止渲染其他内容,然后执行该脚本(异步加载,不按顺序)
- ES6模块:在script标签中,type属性值为module,该模块是异步加载,等同于加上了defer属性
- 可加上async属性
- 对于外部的模块脚本,即单独的js文件,需注意:
- 代码是在模块内部运行的,非全局作用域运行,顶层变量,外部不可见
- 自动采用严格模式
- 可使用import导入其他模块,但必须加上模块文件的后缀名
- 同一个模块多次导入,只执行一次
- 模块中,顶层this返回undefined,而非window;可利用该特性,判断是否为ES6模块
const isNotES6 = this !== undefined
ES6模块与CommonJS模块的差异
- CommonJS模块:
- 输出的是值的拷贝,即当该值发生变化时,外部读取仍然是不变的,除非导出一个取值函数,比如
get counter () { return counter }
- 运行时加载
- require是同步加载
- 使用require()和module.exports
- 输出的是值的拷贝,即当该值发生变化时,外部读取仍然是不变的,除非导出一个取值函数,比如
- ES模块:
- 输出的是值的只读引用,当原始值变了,通过import加载的该值也会同时发生变化;且对该值进行赋值会发生错误
- 编译时加载
- import是异步加载,有一个独立的模块依赖解析阶段
- export导出的接口,被不同的模块导入,引用的都是同一个实例,如果某模块改变了某值,该值在另一个模块中也跟着改变
- 使用import和export
- 模块的加载路径(import命令和package.json中的main字段)必须给出脚本的完整路径(不能省略后缀名)
- nodejs的mjs文件支持url路径,即给路径带上参数(加上一些特殊字符:比如
:
,#
,?
,%
),这样该脚本导入多次,就能够运行多次了;若以若想脚本仅运行一次,必须对特殊符号进行转义
- CommonJS模块与ES6模块互不兼容
Node.js的模块加载方法
- v13.2开始,Node默认打开了对ES6模块的支持
- Node.js要求ES6模块必须采用
mjs
为后缀名:- 只要脚本中采用了export和import,则必须使用
mjs
为后缀名 - Node.js遇到
mjs
后缀文件,默认为es6模块 - 不采用mjs后缀,必须在package.json中指定type字段为module/commonjs(默认)
- 两个模块之间不能混用,只用import才能加载mjs文件(require不能),mjs文件只能使用import命令
- 只要脚本中采用了export和import,则必须使用
package.json
:(感觉是项目打包时使用的)- main字段:指定模块加载的入口文件
- exports字段:优先级高于main字段,可以用来:
- 若exports对象中的key为一点,则优先级高于main字段,代表模块的主入口:
'.': './main.js'
,并可直接简写为:exports: './main.js'
- 给文件起别名:
'./submodule': './src/submodule.js
,例如import sub from 'es-module-package/submodule
等于导入./node_modules/es-module-package/src/submodule.js
- 给目录起别名:
'./folder/': './src/folder/
,用法和1类似 - 未指定别名,则不能使用模块名+脚本名导入文件
- 由于es模块是新版本的node支持的,所以为了兼容旧版本,可同时使用main和exports字段
- 条件加载,为es6模块和commonjs模块指定不同的入口,必须打开
--experimental-conditional-exports
标志🍇
- 若exports对象中的key为一点,则优先级高于main字段,代表模块的主入口:
- commonjs模块加载es6模块:使用async...await和import()🥣
- es6模块加载commonjs模块:只能使用import整体加载🥦:
- 变通方法:使用
module.createRequire()
(不建议使用)
- 变通方法:使用
- 同时支持两种模块的方法:【未执行】
- Nodejs的内置模块:
- 内置模块可以整体加载:import EventEmitter from 'events'
- 内置模块可以局部加载:import { readFile } from 'fs'
- Nodejs的import命令仅能加载本地模块(file协议和data协议),不能加载远程模块,并且脚本路径只支持相对路径,不支持绝对路径
- es6模块是通用的,即在浏览器和服务器环境的效果一致,所以es模块不能使用commonjs模块的一些特俗变量,下面的顶层变量在es模块中不存在:
- 顶层this:es6模块指向undefined,commonjs模块指向当前模块
- arguments
- require
- module
- exports
- __dirname
- __filename
javascript
// 🍇:条件加载
{
'type': 'module',
'exports': {
'.': {
// 指定commonjs的入口
'require': './main.cjs',
// 指定其他模块的入口
'default': './main.js'
},
// 其他别名
'./feature': './src/feature.js'
}
}
// 🥣:commonjs模块加载es6模块
(async () => {
await import('./src/...')
})()
// 🥦:es6模块加载commonjs模块:
import packageMain from 'commonjs-package'
const { main } = packageMain
// 不能使用:
import { main } from 'commonjs-package'
// 变通方法:(不建议使用)
// a.cjs
module.exports = 'packageMain'
// b.mjs
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const packageMain = require('commonjs-package')
packageMain === 'packageMain' // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37