一、什么是ES6模块化
在ES6之前,JS一直没有模块体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法将他们拼装起来。尽管社区制定了一些模块加载方案,主要有CommonJS和AMD两种,但是这两种方案都有自己的局限性,前者主要用于服务器,后者主要用于浏览器。
ES6在语言规格的层面上实现了模块功能,而且实现的相当简单,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。
ES6模块的设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD都只能在运行时确定这些东西。(运行时加载的缺陷是只有在运行时才能得到整个对象,导致无法在编译时进行静态优化)
import {start,exists,readFile} from 'fs';
//上面代码的实质是从fs模块加载这3个方法,而不加载其他方法,这种加载方式称为'编译时加载'或者
//静态加载,即ES6在编译时即可完成模块加载,显然这个效率比CommonJS高
注:ES6的模块自动采用严格模式,不管有没有在模块头部加上'use strict'
二、ES6模块的使用
ES6模块的功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块的功能。
export命令
一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。export命令除了输出变量,还可以输出函数或类。
通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。例如:
function v1(){}
function v2(){}
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
}
上面的代码使用as关键字重命名了函数v1和v2的对外接口,重命名后v2可以用不同的名字输出两次。
需要特别注意的是:export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
//报错,不能直接输出1
export 1;
//报错,依然直接输出1,1只是一个值,不是一个接口
let num = 1;
export num;
可以改成以下写法
export let m = 1;
const num = 1;
export {
m,
num as age
}
他们的实质是,在接口名与模块内部变量之间建立了一一对应的关系。
注意事项:
- export语句输出的接口与其对应的值是动态绑定关系,即通过该接口可以取到模块内部实时的值,而CommonJS模块输出的是值的缓存,不存在动态更新
- export命令可以出现在模块顶层任意位置,如果处于块级作用域内就会报错,import命令也是如此。因为处于条件代码块中就无法做静态优化,这违背了ES6模块的设计初衷。
import命令
使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块了。例如:
import {year,month,date} from './date'
//报错
impourt {'f'+'oo'} from './my_module'
//报错
let module = 'my_module';
import {foo} from module;
注意:import会执行所加载的模块,如果多次重复执行同一句import语句,那么只会执行一次。
模块的整体加载
除了指定加载某个输出值,还可以使用整体加载(即星号*)来指定一个对象,所有输出值都加载在这个对象上。
import * as circle from './circle'
注意:模块整体加载所在的对象应该是可以静态分析的,所以不允许运行时改变。
import * as circle from './circle'
//下面两行是不允许的
circle.foo = 'foo';
circle.area = function (){}
export default 命令
使用import命令时用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档去了解模块有哪些属性和方法。为了方便用户,使其不阅读文档就能加载模块,可以使用export default命令为模块制定默认输出。
export default function(){
console.log('foo')
}
其他模块加载模块时,可以为该匿名函数指定任意名字。
需要注意的是,这时import后面不需要使用花括号。
export default命令用于指定模块的默认输出,显然,一个模块只能有一个默认输出,因此export default命令在一个模块内只能使用一次。本质上,export default就是输出一个叫default的变量或方法,然后系统允许我们为它取任意名字。
三、ES6模块与CommonJS模块的差异
ES6模块与CommonJS模块主要有两大差异:
- CommonJS模块输出的是一个值的复制,ES6模块输出的是值的引用;
- CommonJS模块是运行时加载,ES6模块时静态加载(编译时输出接口)
CommonJS输出的是值的复制,也就是说一旦输出一个值,模块内部的变化就影响不到这个值。例如:
//lib.js
var count = 3;
function add(){
count++;
}
module.exports = {
count:count,
add:add,
}
//main.js
var mod = require('./lib.js);
console.log(mod.count);//3
mod.add();
console.log(mod.count);//3
上面的代码说明lib.js加载后它的内部变化就影响不到输出的mod.count了。这是因为mod.count是一个原始类型的值,会被缓存。
而如果使用ES6模块的话,第二次控制台会打印出4,这是因为ES6的import命令会生成一个只读引用。由于ES6输入的模块变量是一个只读引用,所以对其重新赋值会报错。
//lib.js
export obj = {}
//main.js
import {obj} from './lib.js'
obj.foo = 'foo';//OK
obj = {};//TypeError