ES6 模块化

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};

在外部文件进行引入时,通过namename2两个变量都可以访问到“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

你可能感兴趣的:(ES6 模块化)