Skip to content

module 语法

概述

ES6 之前,最主要模块加载方案 为 CommonJS 和 AMD 。前者用于服务器,后者用于浏览器。

ES6 模块设计思想:尽量静态化,使得编译时就能确定模块的依赖关系。而 CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

CommonJS 模块就是对象,输入时必须查找对象属性

js
// CommonJS模块
let { stat, exists, readfile } = require('fs')

// 等同于
let _fs = require('fs')
let stat = _fs.stat
let exists = _fs.exists
let readfile = _fs.readfile

整体加载 fs 模块,生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,没办法在编译时做“静态优化”

ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入。

js
// ES6模块
import { stat, exists, readFile } from 'fs'

只加载 3 个方法,其他方法不加载。这种加载称为 编译时加载 或者 静态加载,可以在编译时完成,所以可以做静态分析。

引入宏(macro)和类型检验(type system)等静态分析。

ES6 模块还有以下好处

  • 不需要 UMD 模块格式 ,服务器浏览器格式统一

  • 新 API 通过 export 提供,不必做成变量,挂在到全局变量

  • 不再需要对象作为命名空间,如 Math对象

严格模式

ES6 模块自动采用严格模式

严格模式限制

  1. 变量必须声明后再使用
  2. 函数的参数不能有同名属性
  3. 不能使用 with 语句
  4. 不能对只读属性赋值
  5. 不能使用前缀 0 表示八进制数
  6. 不能删除不可删除的属性
  7. 不能删除变量 delete prop,会报错,只能删除属性 delete global[prop]
  8. eval 不会在它的外层作用域引入变量
  9. eval 和 arguments 不能被重新赋值
  10. arguments 不会自动反映函数参数的变化
  11. 不能使用 arguments.callee
  12. 不能使用 arguments.caller
  13. 禁止 this 指向全局对象
  14. 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈
  15. 增加了保留字(比如 protected、static 和 interface)
  16. ES6 模块之中,顶层的 this 指向 undefined,即不应该在顶层代码使用 this

export 命令

模块功能由两个命令构成:export 和 import

export:规定模块的对外接口

import:输入其他模块提供的功能

export 输出变量

js
// profile.js
export var firstName = 'Michael'
export var lastName = 'Jackson'
export var year = 1958
js
// profile.js
var firstName = 'Michael'
var lastName = 'Jackson'
var year = 1958

export { firstName, lastName, year }

export 输出函数或类

js
export function multiply(x, y) {
  return x * y
}

用 as 关键字重命名

js
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

可以多次重命名

export 必须与模块内部的变量建立一一对应关系。即导出的变量对应一个值,而不是直接导出一个值

错误写法:

js
// 报错
export 1;

// 报错
var m = 1;
export m;

第一种写法:直接输出 1

第二种写法:通过变量 m,还是直接输出 1

正确写法:

js
// 写法一
export var m = 1

// 写法二
var m = 1
export { m }

// 写法三
var n = 1
export { n as m }

function 和 class 的输出,也必须遵守这样的写法

js
// 报错
function f() {}
export f;

// 正确
export function f() {};

// 正确
function f() {}
export {f};

export 导出的值是动态绑定,可以 通过 import 获取实时数据

js
export var foo = 'bar'
setTimeout(() => (foo = 'baz'), 500)

上面代码输出变量foo,值为bar,500 毫秒之后变成baz

注:CommonJS 模块输出是值的缓存,不存在动态更新。

export 可以出现在任何位置,但需保证为顶层模块。处于条件代码中,无法做静态优化。

js
function foo() {
  export default 'bar' // SyntaxError
}

import 命令

通过 import 加载 export 导出的模块

as 关键字:将输入变量重命名

js
import { lastName as surname } from './profile.js'

import 命令输入的变量都是只读的

js
import { a } from './xxx.js'

a = {} // Syntax Error : 'a' is read-only;

如果 a 为对象,对象属性可以被修改

import 命令中 from 后路径表示文件位置: 相对路径 或 绝对路径

不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

js
import { myMethod } from 'util'

import 命令会自动提升到整个模块的头部,首先执行。因为 import 是编译阶段执行,代码运行之前。

import 是静态执行,所以不能使用表达式和变量

js
// 报错
import { 'f' + 'oo' } from 'my_module';

// 报错
let module = 'my_module';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

import 语句会执行所加载的模块

js
import 'lodash'

多次重复执行同一句 import 语句,那么只会执行一次

js
import 'lodash'
import 'lodash'
js
import { foo } from 'my_module'
import { bar } from 'my_module'

// 等同于
import { foo, bar } from 'my_module'

模块整体加载

整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面

js
// circle.js
export function area(radius) {
  return Math.PI * radius * radius
}

export function circumference(radius) {
  return 2 * Math.PI * radius
}
js
import * as circle from './circle'

console.log('圆面积:' + circle.area(4))
console.log('圆周长:' + circle.circumference(14))

注意:模块整体加载所在的那个对象(上例是 circle),是可以静态分析,所以不允许运行时改变。

export default 命令

import 命令,需要知道所要加载的变量名或函数名,否则无法加载

export default 命令:为模块指定默认输出。其他模块加载该模块时,import 命令可以为该匿名函数指定任意名字

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

// import-default.js
import customName from './export-default'
customName() // 'foo'

export default 命令用在非匿名函数

js
// export-default.js
export default function foo() {
  console.log('foo');
}

// 或者写成

function foo() {
  console.log('foo');
}

export default foo;

默认输出和正常输出区别

js
// 第一组
export default function crc32() {
  // 输出
  // ...
}

import crc32 from 'crc32' // 输入

// 第二组
export function crc32() {
  // 输出
  // ...
}

import { crc32 } from 'crc32' // 输入

注:一个模块只能有一个默认输出,export default 命令只能使用一次

export default 本质是输出名为 default 的变量。也就是将输出的值,赋值给名为 default 的变量

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

// app.js
import { default as foo } from 'modules'
// 等同于
// import foo from 'modules';

export default 输出类

js
// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();

注:通过 require 处理 ES6 模块,default 属性才是具体值

js
const a = require('./d').default()

export 和 import 复合写法

先输入模块,再将模块原样输出

js
export { foo, bar } from 'my_module'

// 等同于
import { foo, bar } from 'my_module'
export { foo, bar }

注:上述写法是对外转发接口,当前模块是无法取到值的。即 foo,bar 无法直接使用。

模块的接口改名和整体输出

js
// 接口改名
export { foo as myFoo } from 'my_module'

// 整体输出
export * from 'my_module'

默认接口的写法

js
export { default } from 'foo'

默认接口改具名接口

js
export { default as es6 } from './someModule'

// 等同于
import es6 from './someModule'
export { es6 }

具名接口改为默认接口

js
export { es6 as default } from './someModule';

// 等同于
import { es6 } from './someModule';
export default es6;

ES2020 之前,有一种 import 语句,没有对应的复合写法

js
import * as someIdentifier from 'someModule'

ES2020 补上了这个写法

js
export * as ns from 'mod'

// 等同于
import * as ns from 'mod'
export { ns }

模块的继承

模块之间也可以继承

js
// circleplus.js

export * from 'circle'
export var e = 2.71828182846
export default function (x) {
  return Math.exp(x)
}
js
// main.js

import * as math from 'circleplus'
import exp from 'circleplus'
console.log(exp(math.e))

export *命令会忽略 circle 模块的 default 方法

解决:

js
export * from 'circle'
export { default as _default } from 'circle'

跨模块常量

const 声明的常量只在当前代码块有效

设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享

js
// constants.js 模块
export const A = 1
export const B = 3
export const C = 4

// test1.js 模块
import * as constants from './constants'
console.log(constants.A) // 1
console.log(constants.B) // 3

// test2.js 模块
import { A, B } from './constants'
console.log(A) // 1
console.log(B) // 3

import()

问题:import 和 export 命令只能在模块的顶层,不能在代码块之中。但 require 是动态加载,所以 import 无法取代 require 的动态加载功能

js
const path = './' + fileName
const myModual = require(path)

简介

ES2020 提案 引入 import()函数,支持动态加载模块

import 命令能够接受什么参数,import()函数就能接受什么参数。import()返回一个 Promise 对象

js
const main = document.querySelector('main')

import(`./section-modules/${someVariable}.js`)
  .then((module) => {
    module.loadPageInto(main)
  })
  .catch((err) => {
    main.textContent = err.message
  })

import()函数是可以用在任何地方。与 require()方法类似,但前者为异步加载,后者为同步加载

js
async function renderWidget() {
  const container = document.getElementById('widget')
  if (container !== null) {
    // 等同于
    // import("./widget").then(widget => {
    //   widget.render(container);
    // });
    const widget = await import('./widget.js')
    widget.render(container)
  }
}

renderWidget()

适用场合

import()适用场合

  1. 按需加载

import()可以在需要的时候,再加载某个模块。

  1. 条件加载

import()可以放在 if 代码块,根据不同的情况,加载不同的模块

  1. 动态的模块路径

import()允许模块路径动态生成

注意点

import()加载模块成功以后,这个模块会作为一个对象,当作 then 方法的参数

js
import('./myModule.js').then((myModule) => {
  console.log(myModule.default)
})

模块有 default 输出接口

js
import('./myModule.js').then((myModule) => {
  console.log(myModule.default)
})

同时加载多个模块

js
Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});

import()在 async 函数使用

js
async function main() {
  const myModule = await import('./myModule.js')
  const { export1, export2 } = await import('./myModule.js')
  const [module1, module2, module3] = await Promise.all([import('./module1.js'), import('./module2.js'), import('./module3.js')])
}
main()

相关链接

[-] Module 语法