模块化
一个模块就是一个独立的文件,该文件内部的变量、函数、类等内容外部无法访问
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
- 后端模块化规范:CommonJS 同步加载
- 前端模块化规范:异步加载
- 如: AMD require.js , CMD sea.js , ES6 module。
ES6 module
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
- script 标签设置 type="module", 浏览器才会把相关的代码当作ES6的module来对待
- 不能写“裸”路径,即使是同一层级下面的文件,也要加上 './name.js'
- ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"
- ES模块需要在服务器环境运行
- ES6 模块功能主要由两个命令构成:export 和 import
- export命令用于输出模块的对外的功能
- import命令用于输入其他模块提供的功能
export 关键字
如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。 假设以下代码放在 module1.js 文件中
// 不推荐 export var userName = 'xiaoming';
export var userSex = '男';
export var userAge = 27;
// 推荐 var userName = 'xiaoming';
var userSex = '男'; var userAge = 27;
export { userName, userSex, userAge };
export命令除了输出变量,还可以输出对象、函数或类(class)
export function multiply(x, y) { return x * y; };
// 上面代码对外输出一个函数multiply
// 通常情况下,export输出的变量就是本来的名字,可以使用as关键字重命名
function f1() { ... } function f2() { ... } export { f1 as test1, f2 as test2, f2 as test3 };
上面代码使用as关键字,重命名了函数f1和f2的对外接口
重命名后,f2可以用不同的名字输出两次
错误的写法: 报错
export 1;
var m = 1; export m;
function f() {} export f;
正确的写法: 正确
export var m = 1;
var m = 1; export {m};
var n = 1; export {n as m};
export function f() {};
function f() {} export {f};
//export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var abc = '123';
setTimeout(() => abc = '456', 500);
上面代码输出变量abc,值为'123',500 毫秒之后变成'456'。
import 关键字
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。 以下代码假设放在 test1.js 文件中
import { userName, userSex, userAge } from './module1.js';
console.log(userName); console.log(userSex);
console.log(userAge);
如果想为输入的变量重新取一个名字,import命令要使用as关键字将变量重命名。
import { userName as xm } from './module1.js';
import命令输入的变量都是只读的,因为它的本质是输入接口。
也就是说,不允许在加载模块的脚本里面,改写接口。
import {a} from './xxx.js' a = 123; // Syntax Error : 'a' is read-only;
如果a是一个对象,改写a的属性是允许的。
import {a} from './xxx.js' a.foo = 'hello'; // 合法操作
注意,import命令具有提升效果,会提升到整个模块的顶部先执行 show(); import { show } from './module1.js';
除了指定加载某些输出值,还可以使用整体加载,即用星号(*)指定一个对象,
所有输出值都加载在这个对象上面。(不包含默认导出)
import * as obj from './module1.js'; console.log(obj.userName);
console.log(obj.userSex); console.log(obj.userAge);
引入module1.js(拿到这来执行)
import './module1.js'; import './module1.js'; 同一个模块多次导入,不会重复执行,只执行第一次
export default
使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。 为了给用户提供方便,用export default命令,为模块指定默认输出。
下面比较一下默认输出和正常输出:
// 第一组 export default function () { // 输出 // ... } import test from './test.js'; // 输入
// 第二组 export function test() { // 输出 // ... }; import { test } from './test.js'; // 输入
第一组是使用export default时,对应的import语句不需要使用大括号;
第二组是不使用export default时,对应的import语句需要使用大括号。
一个模块只能有一个默认输出,因此export default命令只能使用一次。
因为export default命令其实只是输出一个叫做default的变量,
所以它后面不能跟变量声明语句。
// 正确 export var a = 1;
// 正确 var a = 1; export default a;
// 错误 export default var a = 1;
// 正确 export default 42;
// 报错 export 42;
注意点
浏览器对于带有type="module"的script标签,都是异步加载,不会造成堵塞浏览器 即等到整个页面DOM加载完成,再执行模块脚本,相当于打开了script标签的defer属性
<script type="module" src="./foo.js"></script>
<!-- 类似于 -->
<script src="./foo.js" defer></script>
如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。
ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。
<script type="module">
import utils from "./utils.js";
// other code // ...
</script>
对于外部的模块脚本(如上例的foo.js),有几点需要注意:
- 代码是在模块作用域之中运行,而不是在全局作用域运行
- 模块内部的顶层变量,外部不可见
- 模块脚本自动采用严格模式,不管有没有声明use strict
- 模块之中,可以使用import命令加载其他模块,也可以使用export命令输出对外接口
- 模块之中,顶层的this关键字返回undefined,而不是指向window
- 同一个模块如果加载多次,将只执行一次