ES6
提供了模块化的设计,可以将具有某一类特定功能的代码放在一个文件里,在使用时,只需要引入特定的文件,便可以降低文件之间的耦合性。
相比于早期制定的CommonJS
规范,ES6
的模块化设计有3点不同。
CommonJS
在运行时完成模块的加载,而ES6模块是在编译时完成模块的加载,效率要更高。CommonJS
模块是对象,而ES6模块可以是任何数据类型,通过export命令指定输出的内容,并通过import命令引入即可。CommonJS
模块会在require
加载时完成执行,而ES6
的模块是动态引用,只在执行时获取模块中的值。
ES6
模块核心的内容在于export
命令和import
命令的使用,两者相辅相成,共同为模块化服务。
export命令
export
命令用于定义模块对外输出的内容,任何你想通过外部文件进行访问的内容,只需要通过export
关键字就可以完成。
1. export命令的特性
export
命令的一些特性需要大家重点理解。
(1)export的是接口,而不是值
不能直接通过export
输出变量值,而是需要对外提供接口,必须与模块内部的变量建立一一对应的关系,例如以下写法都是错误的。
let obj = {};
let a = 1;
function foo() {}
export obj; // 错误写法
export a; // 错误写法
export foo; // 错误写法
需要修改成对象被括起来或者直接导出的形式。
let obj = {};
function foo() {}
export let a = 1; // 正确写法
export {obj}; // 正确写法
export {foo}; // 正确写法
(2)export值的实时性
export
对外输出的接口,在外部模块引用时,是实时获取的,并不是import
那个时刻的值。
假如在文件中export
一个变量,然后通过定时器修改这个变量的值,那么在其他文件中不同时刻使用import
的变量,值也会不同。
// 导出文件export1.js
const name = 'kingx2';
// 一秒后修改变量name的值
setTimeout(() => name = 'kingx3', 1000);
export {name};
// 导入文件import1.js
import {name} from './export1.js';
console.log(name); // kingx2
setTimeout(() => {
console.log(name); // 'kingx3'
}, 1000);
2. export命令的常见用法
(1)使用as关键字设置别名
如果不想对外暴露内部变量的真实名称,可以使用as关键字设置别名,同一个属性可以设置多个别名。
const _name = 'kingx';
export {_name as name};
export {_name as name2};
在外部文件进行引入时,通过name
和name2
两个变量都可以访问到“kingx”
值。
(2)相同变量名只能够export一次
const _name = ‘kingx’;
const name = 'kingx';
export {_name as name};
export {name}; // 抛出异常,name作为对外输出的变量,只能export一次
(3)尽量统一export
如果文件export
的内容有很多,建议都放在文件末尾处统一进行export
,这样对export
的内容能一目了然。
const name = 'kingx';
const age = 12;
const sayHello = function () {
console.log('hello');
};
export {name, age, sayHello};
import命令
一个模块中使用export
命令导出的内容,通过import
命令可以引到另一个模块中,两者可以相互配合使用。
如果想要在HTML
页面中使用import
命令,需要在script
标签上使用代码type="module"
。
1. import命令的特性
和export
命令类似,import
命令也有一些特性需要大家重点理解。
(1)与export的变量名相同
import
命令引入的变量需要放在一个大括号里,括成对象的形式,而且import的变量名必须与export
的变量名一致。
这点特性在使用了export default
命令时会有新的表现形式,在后面我们会具体讲到。
// export.js
const _name = 'kingx';
export {_name as name};
// import.js
import {_name} from './export.js'; // 抛出异常
import {name} from './export.js'; // 引入正常
(2)相同变量名的值只能import一次
相同变量名的值只能import
一次,否则会抛出异常。
假如从多个不同的模块中import
进相同的变量名,则会抛出异常,代码如下所示。
// export1.js
export const name = 'kingx';
// export2.js
export const name = 'cat';
// 同时从两个模块中引入name变量,会抛出异常。
import {name} from './export1.js';
import {name} from './export2.js'; // 抛出异常
(3)import命令具有提升的效果
import
命令具有提升的效果,会将import
的内容提升到文件头部。
// export.js
export const name = 'kingx';
// import.js
console.log(name); // kingx
import {name} from './export.js';
在上面的代码中,import
语句出现在输出语句的后面,但是仍然能正常输出。
本质上是因为import
是在编译期运行的,在执行输出代码之前已经执行了import
语句。
(4)多次import时,只会一次加载
每个模块只加载一次,每个JS
文件只执行一次,如果在同一个文件中多次import
相同的模块,则只会执行一次模块文件,后续直接从内存读取。
// export.js
console.log('开始执行');
export const name = 'kingx';
export const age = 12;
// import.js
import {name} from './export.js';
import {age} from './export.js';
在上面的代码中,import
两次export.js
文件,但是最终只输出了一次“开始执行”,可以理解为import
导入的模块是个单例模式。
在上面的代码中,import
两次export.js
文件,但是最终只输出了一次“开始执行”,可以理解为import
导入的模块是个单例模式。
(5)import的值本身是只读的,不可修改
使用import
命令导入的值,如果是基本数据类型,那么它们的值是不可以修改的,相当于一个const常量;如果是引用数据类型的值,那么它们的引用本身是不能修改的,只能修改引用对应的值本身。
// export.js
const obj = {
name: 'kingx5'
};
const age = 15;
export {obj, age};
// import.js
import {obj, age} from './export.js';
obj.name = 'kingx6'; // 修改引用指向的值,正常
obj = {}; // 抛出异常,不可修改引用指向
age = 15; // 抛出异常,不可修改值本身
2. import命令的常见用法
(1)设置引入变量的别名
同样可以使用as
关键字为变量设置别名,可以用于解决上一部分中相同变量名import
一次的问题。
// export1.js
export const name = 'kingx';
// export2.js
export const name = 'cat';
// 使用as关键字设置两个不同的别名,解决了问题
import {name as personName} from './export1.js';
import {name as animalName} from './export2.js';
(2)模块整体加载
当我们需要加载整个模块的内容时,可以使用星号(*)
配合as
关键字指定一个对象,通过对象去访问各个输出值。
// export.js
const obj = {
name: 'kingx'
};
export const a = 1;
export {obj};
// import.js
import * as a from './export.js';
需要注意的是,使用了星号,就不能再使用大括号{}
括起来。以下写法是错误的。
import {* as a} from './export.js'; // 错误的写法
export default命令
在之前的讲解中,使用import
引入的变量名需要和export
导出的变量名一样。在某些情况下,我们希望不设置变量名也能供import
使用,import
的变量名由使用方自定义,这时就要使用到export default
命令了。
// export.js
const defaultParam = 1;
export default defaultParam;
// import.js
import param from './export.js';
console.log(param); // 1
在使用export default命令时,有几点是需要注意的。
1. 一个文件只有一个export default语句
在一个文件中,只能有一个export default
语句,代表一个唯一的默认输出,如果出现多个则会抛出异常。
let defaultParam = 1;
export default defaultParam;
export default 2; // 抛出异常
因为一个文件只能有一个默认的输出,所以在使用import
命令导入时,也可以唯一地确认一个默认的导入值。
2. import的内容不需要使用大括号括起来
在使用import
命令引入默认的变量时,不需要使用大括号括起来。
有没有使用大括号可以用来区分引入的值是否是export default
的值,只有引入export default
对应的值才没有大括号。
以下两个使用import
引入的语句有着本质的区别。
// 表示引入export.js中默认输出的值
import param from './export.js';
// 表示引入export.js文件中输出的变量名为param的值
import {param} from './export.js文件中';
Module加载的实质
ES6模块的运行机制是这样的:当遇到import
命令时,不会立马去执行模块,而是生成一个动态的模块只读引用,等到需要用到时,才去解析引用对应的值。
由于ES6的模块获取的是实时值,就不存在变量的缓存。
// export.js
export let counter = 1;
export function incCounter() {
counter++;
}
// import.js
import {counter, incCounter} from './export7.js';
console.log(counter); // 1
incCounter();
console.log(counter); // 2
第一次输出变量counter
的值时,counter为“1”
,在执行incCounter()
函数后,counter的值加1
,输出“2”。
这表明导入的值仍然与原来的模块存在引用关系,并不是完全隔断的。
这个引用关系是只读的,不能被修改。
import {counter, incCounter} from './export7.js';
console.log(counter); // 1
counter++; // 抛出异常
对上述代码稍做修改,将counter
的值设置为自增,就会抛出异常。
如果在多个文件中引入相同的模块,则它们获取的是同一个模块的引用。
在export.js
文件中定义一个Counter
模块,并导出一个Counter
的实例,代码如下所示。
function Counter() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new Counter();
在另外两个模块中分别导入Counter
模块,并进行不同处理。
// import1.js
import {c} from './export.js';
c.add();
// import2.js
import {c} from './export.js';
c.show();
在一个html
文件中引入两个import
文件。
import './import1.js';
import './import2.js';
通过控制台可以看到,结果输出为“1”。
因为在两个import
文件中使用的c
变量指向的是同一个引用,在import1.js
文件中调用了add()
函数,增加了sum
变量的值,在import2.js
文件中输出sum
变量时,值也变为了1
。