ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
ES6的模块化语法
在模块化被写入 ECMAScript 标准之前, 已经存在各种模块化的实现方式和对应的语法, 例如 AMD, CMD, commonJS 等. 本文只讨论 ES6 标准下的模块化语法.
模块化的必要性
当我们希望全局作用域更加干净,而不是到处都有命名冲突之类的问题;
当我们希望一段代码拥有自己的作用域, 而且不要被其他代码所污染;
当我们希望自己的程序更加井然有序;
这也是 ES6 希望解决的问题, 所以将模块化定为了标准.
模块的概念
模块是一段JavaScript代码, 这段代码会自动运行、而且是在严格模式下、并且无法退出运行。
模块的特点
- ES6 的模块自动采用严格模式,不管你有没有在模块头部加上
"use strict";。 - 模块内定义的变量不会被自动添加到全局作用域
- 由于上面的原因, 模块要向外面暴露一些自己的数据, 这些数据可以被外界访问到
- 一个模块可以从另外一个模块中导入数据(即可以使用其他模块的数据)
- 模块顶层的
this是undefined, 并不是window或global
模块和脚本的区别
模块和脚本初看起来很相似, 他们都可以存在一个单独的文件中, 都可以被其他模块(脚本)引入, 也可以引入其他模块(脚本)的数据, 似乎很难说出他们之间的区别.
但是其实他们的区别非常明显, 用一句话就可以概括: 我们可以按需导入模块中的数据, 但是不能按需导入脚本中的数据。
对于脚本,我们一旦导入了它, 就会将脚本中的所有数据全都导入到了全局作用域中, 这样看起来还是挺乱的。
而对于模块, 则可以只导入我们需要的数据。虽然一个模块可能会暴露出很多的变量、函数和对象, 但我们可以只把需要的那一部分导入进来使用。
从其他模块导入数据和将模块中的数据暴露出去给其他模块的语法 export 和 import
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
将模块中的数据暴露给其他模块使用的语法有两种, 分别如下:
- 先定义一个值,然后将其暴露出去
let name = 'doug';
export name;
function say(){
console.log('hello ' + name);
}
export say;
class Student {
constructor(name, age){
this.name = name;
this.age = age;
}
}
export Student;
// 定义一个函数,而且不把它暴露出去
function privateFunc(){
console.log('我是私有成员,没有被暴露出去,所以别人没有办法访问到我');
}
复制代码上面的操作可以分为两个过程, 先定义、再暴露。其实可以简化为一句代码: 2. 在定义变量的同时暴露数据, 只要在声明符之前加上 export 就可以.
// 定义一个变量并暴露
export let name = 'doug';
// 定义一个函数并暴露
export function say(){
console.log('hello ' + name);
}
// 定义一个类并暴露
export class Student {
constructor(name, age){
this.name = name;
this.age = age;
}
}
// 定义一个函数,并不把它暴露出去, 其他模块从外部无法访问这个函数
function privateFunc(){
console.log('我是私有成员,没有被暴露出去,所以别人没有办法访问到我');
}
复制代码注意, 不暴露出去的数据是从外部访问不到的, 例如上面例子中的
privateFunc函数.
export的写法,除了像上面这样,还有另外一种。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,
从别的模块导入数据 import
从别的模块得到数据以供自己使用, 要用 import 关键字。在导入之前, 要先明确两个问题: 一是从哪个模块导入? 二是想导入模块中的什么数据?
在明确从哪导入和导入什么之后,就可以按照下面的语法来写了:
import { 数据1, 数据2, ... , 数据n } from 模块名
复制代码例如,从 a 模块中导入 name 变量 和 say 函数:
import { name, say } from './a.js';
复制代码这样就把 name 和 say 从 a 模块导入进本模块中了,现在就可以使用它们了, 例如输出 name 的值, 或者调用 say 这个函数:
console.log(name);
say();
复制代码脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。,a的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。import命令具有提升效果,会提升到整个模块的头部,首先执行。
关于导入的注意事项
导入的数据是没有办法直接改变的。比如,我们无法直接给 name 重新赋值, 这样会报错。
import { name } from './a.js';
name = 'new name'; // Error 错误
复制代码但是可以间接的修改这个变量的值. 假如在模块 a 中定义了一个可以修改 name 值的函数, 则可以通过调用这个函数来修改 name 的值了. a 模块中的内容:
... 包括 name 在内的其他数据
function updateName(newName){
name = newName;
}
复制代码导入 a模块的 updateName 函数来修改 name 的值:
import { name, updateName } from './a.js';
updateName('new Name'); // name的值已经被修改为了 "new name"
复制代码一次性导入一个模块暴露出来的所有数据
当想用一行代码导入一个模块暴露出来的所有数据时, 用上面的方法可能比较困难, 因为一个模块暴露出来的方法可能很多, 把每个变量名都写出来的方法就不够灵活了.
即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
这个时候可以用命名空间导入的方法, 即将一个模块暴露出来的所有的数据挂载到一个对象上,通过这个对象来调用这些数据, 例如:
import * as aObj from './a.js'; // a 模块所有暴露出的数据都挂到了 aObj 上
// 通过 aObj 来访问和调用 a 模块中的变量和方法
console.log(aObj.name);
aObj.say();
复制代码使用 as 对导入和要暴露出去的数据重命名
如果想将数据以别的名称暴露出去, 可以使用 as 关键字, 语法为: export { 原名称 as 新名称 } .
这样在其他模块中就可以用这个新的名称来导入这个数据了, 例如将函数 say 以 speak 为名称暴露出去:
export {say as speak};
复制代码那么其他模块就要用 speak 来导入这个函数了: import { speak } from './a.js'
在导入一个数据时, 也可以对它重命名, 并使用重命名之后的名称来访问它, 例如将它命名为 Howling, 之后就可以使用 howling 来访问这个函数了:
import { speak as howling} from './a.js'; // 导入时重命名
howling(); // 用新名字调用这个函数
复制代码将一个值作为默认值暴露出去 default
每个模块可以将一个变量、函数或者类作为默认值暴露出去.
将一个值作为默认值暴露出去的语法有3种,分别是:
- 定义的时候就暴露
- 先定义后暴露
- 将这个值用 as 命名为 default 后暴露
// 1. 定义的时候就暴露
export default function(){
console.log('方法1暴露函数为默认值');
}
// 2. 先定义后暴露
let name = 'doug';
export default name;
// 3. 将这个值用 as 命名为 default 暴露
class Student{ // ... }
export {Student as default};
复制代码导入其他模块暴露出的默认值
导入默认值的语法和导入非默认值的语法略有不同. 导入一个模块暴露出的默认值, 并不用 {} 来包裹, 而且不用使用 as 关键字就可以将这个默认值重命名: 例如导入上面例子中的 name, 并将其重命名为 familyName
import familyName from './a.js';
复制代码同时导入默认值和非默认值的语法
在一行语句中同时导入默认值和非默认值的语法非常简单, 就是将这两种导入的方式结合起来, 非默认值用{}括起来, 而默认值不用. 语法为:
import 默认值 { 非默认值 } from './a.js';
复制代码将从其他模块导入的数据暴露出去
有时候需要将从其他模块导入的某些数据再暴露出去, 这有两种语法
- 先导入, 再暴露
import { name } from './a.js'; // 导入
export {name}; // 暴露
复制代码- 一行代码之内同时导入和暴露
export { name } from './a.js';
复制代码也可以重命名后再暴露出去
export { name as lastName } from './a.js';
复制代码由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 报错
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';
}脚本和模块的异步
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
<script>标签打开defer或async属性,脚本就会异步加载。
渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
defer与async的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
浏览器对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。
import
最后,import语句会执行所加载的模块,因此可以有下面的写法。
import 'lodash';
上面代码仅仅执行lodash模块,但是不输入任何值。
如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。
import 'lodash';
import 'lodash';import顺序问题以及骨架屏
当我们把html内容都放到js模块中,只留下模块接口,主页html会只剩下一个空白页面,在网速特别慢时,只会有一片空白,这是我们可以放一块图片进去。等待模块加载完成之后,删掉图片,通常加上加载图或者骨架屏(实际上骨架屏是通过服务端渲染技术SSR实现的)
import模块会提升到代码最顶层,而且是在编译时就会执行,就会产生一个结果,在前面的import相当于代码写在前面,最先执行
import()懒加载
前面介绍过,import命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行,,import和export命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果import命令要取代 Node 的require方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能。
import()返回一个 Promise 对象。import()是异步的