Module语法
背景:在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
引入:下面代码实质是整体加载fs
模块(即加载fs
的所有方法),生成一个对象(_fs
),然后再从这个对象上面读取3个方法。这种加载称为**“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。
下面代码实质是从fs
模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载。
import { stat, exists, readFile } from 'fs';
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
严格模式
ES6 的模块自动采用严格模式:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with
语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀0表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象 - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
export 命令
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。export
命令用于规定模块的对外接口,如果你希望外部能够读取模块内部的某个东西,就必须使用export
关键字输出:
-
export
可以输出变量、函数和类//输出变量 export var firstName = 'Michael'; //输出函数 export function myfun() {}; //输出类 export class myclass {};
-
export
的第二种写法var firstName = 'Michael'; function myfun() {}; class myclass {}; export {firstName, myfun, myclass};
-
export
输出的接口名称可以使用as
关键字重命名function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, };
-
export
命令规定的是对外的接口,而不是直接输出的数据或立即执行的命令// 报错 export 1; // 报错 var m = 1; export m; // 报错 function f() {} export f;
// 正确 export var m = 1; // 正确 var m = 1; export {m}; // 正确 export function f() {}; // 正确 function f() {} export {f};
-
export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值 -
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以function foo() { export default 'bar' // 非顶层,报错 } foo()
import 命令
import
命令用于输入其他模块提供的功能。使用export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块。
-
用
import
命令从其他文件(或模块)中引入变量(函数或类) -
import
命令接受一对大括号,里面指定要从其他文件(或模块)导入的变量名(函数名或类名),名称必须与模块对外接口的名称相同import {firstName, lastName, year} from './profile';
-
import
命令可以使用as
关键字,给输入的变量(函数或类)重命名import { lastName as surname } from './profile';
-
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。import {myMethod} from 'util'; //util是个模块,引入前提是有配置文件 import 'lodash'; lodash是个模块,引入前提是有配置文件
-
import
命令具有提升效果,会提升到整个模块的头部,首先执行foo(); import { foo } from 'my_module'; //会提升到foo前面,不会报错
模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*
)指定一个对象,所有输出值都加载在这个对象上面
//circle.js:输出两个方法area和circumference
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
//main.js:加载circle.js模块
//1. 逐一加载
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
//2. 整体加载
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
export default 命令
-
export default
命令为模块指定默认输出// export-default.js:默认输出一个匿名函数 export default function () {}
-
import
加载该模块时,可以为该匿名函数指定任意名字。这时import
命令不使用大括号// import-default.js:加载模块 import customName from './export-default';
-
export default
命令也可以输出非匿名函数export default function foo() {} //or function foo() {} export default foo;
虽然有名称,但在模块外部是无效的。加载的时候,视同匿名函数加载
-
一个模块只能有一个默认输出,因此
export default
命令只能使用一次 -
本质上,
export default
输出的是一个叫做default的变量或方法,所以它后面不能跟变量声明语句// 正确 export var a = 1; // 正确 var a = 1; export default a; //意为将变量a的值赋给变量default // 错误 export default var a = 1; // 正确 export default 42; // 报错 export 42;
-
import
可以同时输入默认方法和其他变量import _, { each } from 'lodash'; //前面的_表示默认输出的方法
与上面对应的
export
语句如下export default function (obj) {} export function each(obj, iterator, context) {} //暴露出forEach接口,默认指向each接口,即forEach和each指向同一个方法 export { each as forEach };
-
export default
可以用来输出类export default class { ... }
export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import
语句可以与export
语句写在一起
import { foo, bar } from 'my_module';
export { foo, bar };
//可以写成:
export { foo, bar } from 'my_module';
// 整体输出
export * from 'my_module';
跨模块常量
const
声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法:
// 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
如果要使用的常量非常多,可以建一个专门的constants
目录,将各种常量写在不同的文件里面,保存在该目录下
// constants/db.js
export const db = {
url: 'http://my.couchdbserver.local:5984',
admin_username: 'admin',
admin_password: 'admin password'
};
// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
然后,将这些文件输出的常量,合并在index.js
里面
// constants/index.js
export {db} from './db';
export {users} from './users';
使用的时候,直接加载index.js
就可以了
// script.js
import {db, users} from './constants';
import()
引入问题
import
命令会被 JavaScript 引擎静态分析,先于模块内的其他模块执行。所以下面代码会报错:
if (x === 2) {
import MyModual from './myModual';
}
//引擎处理import语句是在编译时,这时不会去分析或执行if语句。所以import语句放在if代码块之中毫无意义
import
和export
命令只能在模块的顶层,不能在代码块之中。这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。从语法上,条件加载就不可能实现。
解决
import()
函数,实现运行时加载模块,即动态加载
import(specifier) //参数specifier,指定所要加载的模块的位置
import
命令能够接受什么参数,import()
函数就能接受什么参数,两者区别主要是后者为动态加载
import()
返回一个 Promise 对象的例子
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
import()
是运行时执行,也就是说,什么时候运行到这一句,也会加载指定的模块。import()
类似于 Node 的require
方法,区别主要是前者是异步加载,后者是同步加载。
适用场合
-
按需加载
button.addEventListener('click', event => { import('./dialogBox.js') .then(dialogBox => { dialogBox.open(); }) .catch(error => { /* Error handling */ }) }); //import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块
-
条件加载
if (condition) { import('moduleA').then(...); } else { import('moduleB').then(...); }
-
动态的模块路径
import(f()) .then(...); //根据函数f的返回结果,加载不同的模块
注意
import()
加载模块成功以后,这个模块会作为一个对象,当作then
方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
//export1和export2都是myModule.js的输出接口,可以解构获得
如果模块有default
输出接口,可以用参数直接获得。
import('./myModule.js')
.then(myModule => {
console.log(myModule.default);
});
如果想同时加载多个模块,可以采用下面的写法
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
import()
也可以用在 async 函数之中
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();