# ES6模块化

ES6模块不是对象,而是通过export命令显示地指定输出的代码,再通过import命令输入。这种加载称为编译时加载或者静态加载。这样能保证在编译时就确定模块之间的依赖关系,每个模块的输入和输出也都是确定的。而CommonJs/AMD模块无法保证在编译时确定这些内容,都是运行时确定。

TIP

  • 静态性带来的限制
    • 只能在文件顶部引入依赖
    • 导出的变量类型受到严格限制
    • 变量不允许被重新绑定,引入的模块名只能是字符串变量,即不可以动态确定依赖。

这样的限制在语言层的好处就是:我们可以通过分析作用域,得出代码的变量所属的作用域以及他们之间的引用关系,进而可以推导出变量和导入依赖变量之间的引用关系,在没有明显的引用时,可以对代码进行去冗余。

  • 设计成静态的好处:
    • 通过静态分析,我们能够分析出导入的依赖。如果导入的模块没有被使用,我们便可以通过tree shaking等手段减少代码提交,进而提升运行性能。

# 严格模式

ES6模块自动采用严格模式;

  • 变量必须声明后使用
  • 函数的参数不能有同名属性
  • 不能使用with语句
  • 不能删除变量delete prop
  • 禁止使用this指向全局对象
  • 不能使用fn.caller/fn.arguments获取函数调用的堆栈 等限制。
// profile.js
var firstName = 'linjiaheng'
var lastName = 'dovis'

export {firstName, lastName}
1
2
3
4
5

优先考虑这种导出的方式,可以在脚本底部直观看出导出哪些变量。

# 模块导出规则

  1. 通常export输出的变量就是本来的名字,可以使用as关键字重命名。可以用不同名字输出多次。
  2. export命令规定的是对外的接口,必须与模块内部的变量建立一一对应的关系
// 报错
export 1;
// 报错
var m = 1;
export m

// 正确
export const m = 1;

var m = 1
export {m}

var n = 1
export {n as m}
//后面三种正确方式规定了对外的接口m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. export语句输出的接口与其对应的值是动态绑定关系的,即通过该接口可以取到模块内部实时的值。
  2. export语句可以出现在模块的任何位置,只要处于模块顶层就可以。
  3. export通过接口输出的是同一个值,不同的脚本加载这个接口得到的都是同样的实例。

# 模块导入规则

  1. 导入的大括号中的变量名必须与被导入模块对外接口的名称相同。如果想为输入的变量重新取一个名字,要在import命令中使用as关键字,将输入的变量重命名。
  2. import命令具有提升效果,会提升到整个模块的头部并首先执行。这种行为的本质是import命令是编译阶段执行的,在代码运行之前。
  3. import静态执行,所以不能使用表达式和变量。只有在运行时才能得到结果的语法结构。
  4. import语句会执行所加载的模块,所以这种情况成立。多次重复执行同一句,只会执行一次。
import 'lodash'
1
  1. 如果同一个模块,同时使用了CommonJS的require命令和ES6的import语句,由于import在静态解析阶段中执行,所以最早被执行。
  2. import * as 名称 from ''用来加载整个对象
  3. import无法在运行时加载模块。

# export default

用该语句导出的接口,可以用import语句指定任意名字,不使用大括号。因为export default只能使用一次。

本质上,export default就是输出一个叫做default的变量或方法,然后系统运行我们为他取任意名字,所以可以这么写;

// module.js
function add(x,y) {
    return x +y
}
export {add as default}
// 等同于
export default add;

// app.js
import {default as xxx} from 'modules'
// 等同于
import xxx from 'modules'
1
2
3
4
5
6
7
8
9
10
11
12

正是因为export default语句其实就是输出一个叫做default的变量,所以后面不能跟变量声明语句

// 错误
export default var a = 1;

// 正确
var a = 1
export default a;
1
2
3
4
5
6

# import和export复合写法

如果在同一个模块之中先输入后输出同一个模块,import语句可以与export语句写在一起

export {foo,bar} from 'my_module'

// 等同于
import {foo,bar} from 'my_module'
export {foo, bar}
1
2
3
4
5

# import()

因为import命令无法动态加载模块,所以提案引入了import()函数完成动态加载。import命令可以接收什么参数,该函数就可以接收什么参数。 import()函数类似Noderequire语句,区别是前者异步加载,后者同步加载。

  • 该函数的适用场合
    • 在需要的时候加载某个模块---按需加载
    button.addEventListener('click', event => {
        import('./dialogBox.js')
        .then(dialogBox => {
            dialogBox.open()
        })
    })
    
    1
    2
    3
    4
    5
    6
    • 条件加载
    • 动态的模块路径
    import(f())
    .then(...)
    // 根据函数f的返回结果加载不同的模块。
    
    1
    2
    3

import()加载模块成功之后,这个模块会作为一个对象当作then方法的参数,因此可以适用对象解构赋值的语法获取输出接口。

# Node加载

  1. 在静态分析阶段,一个模块脚本只要有一行import/export语句,Node就会认为该脚本为ES6模块,否则为CommonJS模块。如果不输出任何接口,但是希望Node认为为ES6模块,可以:
export {}
1
  1. node从9.0版本开始支持ES模块化,执行脚本启动时需要加上--experimental-modules。不过这要求文件名后缀为.mjs。另外一种使用ES模块化的方式是安装babel-clibabel-preset-env,配置.babelrc文件后,执行./node_modules/.bin/bable-node或者npx babel-node

# 在浏览器中快速使用ES模块化

只需要在script标签上添加一个type=module属性。不支持ES模块的浏览器,只需要通过nomodule属性来执行某脚本的回退方案。