普通的 JS 文件没有模块作用域,没有边界,共享一个全局作用域,非常容易变量冲突,难以维护。所以引入模块,一个模块就是一个文件ES6

区别

模块化文件相比普通脚本的区别:

  • 默认严格模式
  • 每个模块都有自己的顶级作用域
  • 如果同一个模块被多次导入,那么它的代码只会在第一次被导入时执行一次
  • 模块只能通过 HTTP(s) 引入,不能从本地文件引入
  • this 是 undefined
  • 在浏览器中
    • 使用模块化脚本需要添加 type=“module”:<script type="module">
    • 总是延时加载的,类似 defer
    • 外部和内联模块脚本都可以添加 async(比 defer 优先级高;且普通脚本只能外部引入才能添加 async)
    • 从另一个源加载的外部脚本需要 CORS header
模块脚本普通脚本
严格模式默认严格模式默认非严格模式
作用域有自己的顶级作用域全局作用域
多次导入只会在第一次被导入时执行一次会执行多次
顶层thisundefinedwindow(严格和非严格都是)

import.meta 对象包含关于当前模块的信息

导出(export)

export 可以出现在任何位置,只要在顶层作用域就行,如果处于块级作用域内,就会报错(没法做静态优化)
没有任何 import 或者 export,但是希望它被作为模块处理可以在后面加上 export {}

1. 声明时导出

export const VALUE = 1
 
export function hello() {
  console.log('hello world')
}

2. 导出和声明分开

function hello() {
  console.log('hello world')
}
 
export { hello } // 导出变量列表,加大括号

3. 导出别名

function hello() {
  console.log('hello world')
}
 
export { hello as h } // 使用 as

4. 默认导出

一个模块只能有一个默认导出,默认导出可以不要变量名称(其他文件引入时使用 default 名称),不用大括号
本质上,export default 就是输出一个叫做 default 的变量或方法

export default function foo() {
  console.log('hello world')
}
 
// 或者
function foo() {
  console.log('hello world')
}
export default foo
 
// 或者
function foo() {
  console.log('hello world')
}
export { foo as default }

5. 重新导出

导入内容,并立即将其导出,一般用于将多个文件的导出进行汇总到 index 然后再统一导出
重新导出的模块在当前文件中不可用

export { a } from './a.js' // 重新导出命名导出
// 类似下面,但是没有实际导入
import { a } from './a.js'
export { a }
 
export * from './b.js' // 重新导出所有命名导出(不含默认导出)
export { default } from './c.js' // 重新导出默认导出
export { default as m } from './d.js' // 重新将默认导出为命名导出
 
export B from './b.js' // error! 这是一个错误语法(不能直接这样默认导出,只能按上面那样)

导入(import)

import 必须给出相对或绝对的 URL 路径。没有任何路径的模块被称为“裸(bare)”模块,在 import 中不允许这种模块。(打包工具可以)

1. 直接导入

不会引用任何变量到当前模块,但是会执行导入的文件中的所有代码

import './file.js'

2. 单个导入

import { hello } from 'a.js'
 
hello()

3. 全部导入

import * as A from 'a.js'
 
A.hello() // 引入的命名导出
A.default // 引入的默认导出

4. 别名导入

import { hello as h } from 'a.js'
 
h()

5. 默认导入

默认导入不需要大括号,同时可以自定义名称

import myName from 'a/js'
 
myName()

Tip

  • import 命令具有提升效果,会提升到整个模块的头部,在编译阶段执行,在代码运行之前,由于 import 是静态执行,所以不能使用表达式和变量
  • 导入对象可以改写里面的属性,并且其他模块也可以读到改写后的值。可以用于配置一个公共变量,但是这种写法很难查错

动态导入ES2020

上面的语法都是静态导入,语法简单且严格
动态按需导入模块使用 import(module):加载模块并返回一个 promise,该 promise 的 resolve 为一个包含其所有导出的模块对象

let { hi, bye } = await import('./a.js') // 命名导入
hi()
bye()
 
let obj = await import('./a.js')
let a = obj.default // 默认导入

完整示例:

// say.js
export function hi() {
  alert(`Hello`);
}
 
export function bye() {
  alert(`Bye`);
}
 
export default function() {
  alert("Module loaded (export default)!");
}
<!doctype html>
<script>
  async function load() {
    let say = await import('./say.js');
    say.hi(); // Hello!
    say.bye(); // Bye!
    say.default(); // Module loaded (export default)!
  }
</script>
<button onclick="load()">Click me</button>

Tip

import() 函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用
主要用途:

  • 按需加载
  • 条件加载
  • 动态的模块路径

注意:import() 并不是一个函数调用,只是一种特殊语法