JavaScript一直没有模块(module)体系,无法将一个大程序拆分成相互依赖的小文件,再用简单的方法拼装起来,这对开发大型的、复杂的项目形成了巨大的障碍。在ES6之前,社区制定了一些模块化加载方案,最主要的有CommonJS
和AMD
两种,前者用于服务器,后者用于浏览器,ES6在语言标准的层面上,实现了模块功能,完全可以取代CommonJS和AMD规范,成为浏览器和服务器通用的解决方案。
ES6模块的设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入输出变量。CommonJS和AMD模块都只能在运行时确定这些东西,比如CommonJS的模块就是对象,输入时必须查找对象属性:
1 | // CommonJS模块 |
上面实质上是整体加载fs
模块(即加载fs
的所有方法),生成一个对象,然后再从这个对象上读取三个方法。这种加载称为“运行时加载”,因为只有在运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入:
1 | import {stat, readFile} from 'fs'; |
上面实质上是从fs
模块中加载两个方法,其他的不加载,这种加载称为“编译时加载”或“静态加载”,即ES6可以在编译时就完成模块加载,效率比CommonJS
模块的加载方式更高,也使得静态分析成为可能。
ES6的模块自动采用严格模式,不管你在模块头部有没有加上"use strict";
。严格模式的主要限制有:
- 变量名必须声明后使用
- 函数参数不能有同名属性
- 不能使用with语句
- 禁止
this
指向全局,顶层的this指向undefined
ES模块功能主要由两个命令构成:
- export 规定模块的对外接口
- import 输入其他模块提供的功能
export命令
一个模块就是一个独立的文件,该文件内部的所有变量,外部都无法获取。如果希望外部能够读取模块内部的某个变量,就必须使用export
关键字输出该变量。例如:
1
2
3
4 // test.js
export const firstName = 'Michael';
export const lastName = 'Jackson';
export const year = 1958;
或者可以等价写为:
1
2
3
4const firstName = 'Michael';
const lastName = 'Jackson';
const year = 1958;
export {firstName, lastName, year};
通常情况下,export
输出的变量就是本来的名字,但是也可以使用as
关键字进行重命名。
特别注意:**exprt
命令规定的事对外的接口,必须与模块内部的变量建立意义对应的关系。**
1 | export 1; // 错误直接输出1,而不是接口 |
同样的,function
和class
的输出也必须遵守这样的写法:
1 | function f () {}; |
另外:**export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部的实时的值。**
1 | export let foo = 'bar'; |
上面输出变量foo
,值为bar
,500毫秒之后变为baz
。
在CommonJS中,模块输出的是值的缓存,不存在动态更新。
export
命令可以出现在模块的任何位置,只要处于模块的顶层就可以。如果处于作用域内,就会报错,包括import
命令也是如此,因为如果处于条件代码块中,就没法做静态优化,违背了ES6模块的设计初衷。
import 命令
使用了export
命令定义了模块的对外接口以后,其他js文件就可以通过import
命令加载这个模块了。
import
命令接受一对大括号,里面指定要从其他模块中导入的变量名,大括号里面的变量名,必须与被导入模块的对外接口的名称相同(可以使用as
关键字将输入的变量进行重命名):
1 | // 从test模块导入定义的三个变量 |
import
命令输入的变量都是只读的,因为它的本质是输入接口。不允许在加载模块的脚本里面改写接口:
1 | import {firstName, lastName, year as y} from './test'; |
但是如果导入的是一个对象,那么修改对象的属性是允许的,但是不建议改,都应该当做只读,否则会出现bug很难查错。
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
可省略。如果只是模块名,不带有路径,那么必须通过配置,告诉引擎怎么取到这个模块。
import
命令具有提升效果,会提升到整块的头部,首先执行。因为本质是,import
命令是编译阶段执行的,在代码之前。并且,由于import
是静态执行的,所以不能使用表达式和变量。
最后,import
语句会执行所加载的模块,如果多次重复执行同义句import
语句,那么只会执行一次,不会执行多次。
模块的整体加载
除了制定加载某个输出值,还可以整体加载,即用*
指定一个对象,所有输出值都加载在这个对象上面。例如:
1 | import * as test from './test'; |
但是要注意的是:模块整体加载所在的对象上面,应该是可以静态分析的,所以不允许运行时改变。
export default 命令
前面看了,使用import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,也为了方便某些框架自动加载,可以使用export default
命令,为模块指定默认输出:
1 | // test.ts |
这样,其他模块加载该模块时,import
命令可以指定为任何名字:
1 | import test from '../interface/test'; |
上的import
命令可以使用任意名字指向模块的输出,这时就不需要知道原模块的输出名字是什么,需要注意的是:使用export default
后,导入命令import
就不需要使用大括号了。
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此,export default
命令只能使用一次。所以import
命令后面才不用加大括号,因为只能唯一对应export default
命令。
export default
和export
不同,正因为export default
命令其实输出一个叫default
的变量,所以它后面不可跟变量声明语句,这正好和export
相反:
1 | export var a = 1; // 正确 |
如果想在一条import
语句中,同时输入默认方法和其他接口,可以写成:
1 | // test.js |
1 | // 使用 |
总结
在写angular的时候,知道要export
对应的class,在写eggjs
的时候,有export default
,但是不知道为啥这么用,通过学习ES6模块这部分,来细致了解下。