ES6的Class只是面向对象编程的语法糖,升级了ES5的构造函数的原型链继承的写法,并没有实际的解决模块化的问题。
module功能就是为了解决这个问题而提出的。
1.export命令
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件,如果希望外部能够读取模块内部的某个变量,就必须使用export关键字输出这个变量。如下:
//写法一
export var first="michael";
export var last = "lion";
export var year = "mouse";
//写法二
var first="michael";
var last = "lion";
var year = "mouse";
export {first,last,year};
如此就用export命令对外部输出了3个变量,这两种写法都是等价的,但是应该优先考虑使用写法二,因为这样写就可以在脚本尾部一眼看清输出了哪些变量。
其次,export命令除了输出变量,还可以输出函数或者类。
如:
export function multiply(x,y){
renturn x*y;
} //对外输出函数multiply
通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。
如下:
function v1(){ ... }
function v2(){ ... }
export {
v1 as streamV1;
v2 as streamV2;
v2 as streamLastVersion
};
上面的代码使用as关键字重命名函数V1和V2的对外接口,重命名后,V2可以用不同的名字输出两次。
最后,export命令可以出现在模块的任何位置,只要处于模块顶层即可。
export语句输出的值是动态绑定,绑定其所在的模块。
export var foo = "bar";
setTimeout(() =>foo = "baz" ,500);
上面的代码输出变量foo,值为bar,500毫秒之后变成baz。
2.import
使用export命令定义了模块的对外接口之后,其他js文件就可以通过import命令加载这个模块。
如:
import {first,last,year} from './profile';
function setName(element){
element.textContent = first + ' ' + last;
}
上面的import就是用来加载profile.js文件,并从中输入变量。
import命令接受一个对象,用{}表示,里面制定要从其他模块导入的变量的名字,{}中的变量名字必须与被导入的模块对外接口的名称相同。
如果想为变量重新取一个名字,要在import中使用as关键字,将输入的变量重新命名。
import {last as surname} from './profile';
注意: import 命令具有提升效果,会提升到整个模块的头部首先执行。
例如:
foo();
import {foo} from 'my_modult';
上面的代码不会报错,因为import优先于foo之前执行。
如果在一个模块中先输入后输出同一个模块,import语句可以和export语句写在一起。
例如:
export {es6 as default} from './someModule';
//相当于:
import {es6} from './someModule';
export default es6;
上面的代码中,import语句可以和export语句写在一起,但是从性能的角度考虑,这样做是不太好的,所以一般采用标准写法。
3.模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,只要使用(*)制定一个对象,这样所有输出值都会加载在这个对象上。
例如,下面的first.js文件,输出两个方法:area和circumference:
//circum.js
export function area(radius){
return Math.PI*radius*radius;
}
export function circumference(radius){
return 2*Math.PI*radius;
}
//现在加载这个模块
//main.js
import {area,circum} from './circle';
console.log("圆形面积:"+area(4));
console.log("圆周长:"+circle.circumference(14));
上面的写法是逐一指定加载扥方法,整体加载的写法如下:
import *as circum from './circle';
console.log("圆形面积:"+area(4));
console.log("圆周长:"+circle.circumference(14));
4.module命令
module命令可以代替import语句,达到整体输入模块的作用。
//main.js
module circum from './circle';
console.log("圆形面积:"+area(4));
console.log("圆周长:"+circle.circumference(14));
module后跟一个变量,表示输入的模块定义在该变量上。
5.export default命令
为了方便使用者,使其不用阅读文档就能加载模块,就要用到export defalut命令,为模块指定默认输出。
例如:
//export-default.js
export default function (){
console.log("foo");
}
上面的模块文件export-defalut.js的代码,它的默认输出是一个函数。
在其他模块中输出该模块时,import命令可以为该匿名函数指定任意名字,如下:
import-default.js
import customName from './export-default.js'
customName(); //foo
上面的import命令可以用任意名称指向export-default.js输出的方法,这样就不用必须知道元模块的函数名字了。但是注意的是,这时的import后面不用使用{}括起来。
export default命令用在非匿名函数前也是可以的。
如下:
//export-defalut.js
export default function foo(){
console.log("foo");
}
//或者:
function foo(){
console.log("foo");
}
export default foo;
可以这样做的原因,只是因为,foo函数在模块外部是无效的,加载的时候将其视同为匿名函数。
比较:
使用export default命令时,对应的import语句不需要使用大括号,但是只使用export命令时,import是要加括号的。
理由:
export defalut命令用于指定模块的默认输出,当然,一个模块只能有一个默认输出,因此export default命令只能使用一次,所以,其对应的import命令后面才不用加上大括号,因为只可能对应一个方法。
本质上,export default命令就是输出一个叫做default的变量或者方法,然后系统允许你为她起任意的名字,所以下面的写法也是有效的:
//module.js
function add(x,y){
return x*y;
};
export {add as default};
//app.js
import {default as xxx} from './module';
有了export dedfault命令,输入模块时就非常直观了。
其次,如果想在一条import语句中同时输入默认方法和其他变量,可以如下写法:
import customName,{otherMethod} from './export-default'
如果要输出默认值,只需要将值跟在export default之后即可。
//export default也可以用来输出类
export default 42
//myclass.js
export default class{...}
//main.js
import myclass from 'myclass'
let o = new myclass();
6.模块的继承
模块之间也是存在继承的,比如:
假设有一个circleplus模块继承了circle模块。
//circleplus.js
export *from 'circle';
export var e=2.718;
export default function(x){
return Math.exp(x);
}
上面的export *就表示输出circle模块的所有属性和方法。
注意:
export *命令会忽略circle模块的default方法,之后又输出了自定义的e变量和默认方法。
这时也可以将circle的属性或方法改名后再输出。
//circleplus.js
export {area as circleArea}from 'circle';
上面的代码表示,只输出circle模块的area方法,且将其改名为circleArea。
加载上面的模块写法如下:
//main.js
module math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.E));
上面的import exp表示,将circleplus模块的默认方法加载为exp方法。
7.ES6模块加载的实质
首先,ES6模块加载机制与commonJs模块完全不同。
commonJs模块输出的是一个值的复制品,但ES6模块输出的是值的引用。
commonJs模块输入的是被输出值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个被输出的值了。如:
//lib.js
var counter = 3;
function incCounter(){
counter++;
}
moudle.exports = {
counter:counter,
incCounter:incCounter,// 输出内部变量counter和改写这个变量的内部方法incCounter
};
//然后加载该模块:
//main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); //3
incCounter();
console.log(counter); //3
这段代码就表示,在counter输出后,lib.js模块内部的变化就影响不到counter了。
其次:
ES6模块的运行机制与commonJs不一样,它遇到模块加载命令import时不会去执行模块,只会生成一个动态的只读索引,等到真的要用到这个值的时候,才会到模块中去取值,所以只要原值变了,输入值也是会跟着改变的。
因此,ES6模块是动态引用,并不会缓存值,模块里面的变量绑定其所在的模块。
例如:
//lib.js
export let counter = 3;
export function incCounter(){
counter++;
}
//main1.js
import {counter,incCounter} from './lib';
console.log(counter); //3
incCounter();
console.log(counter); //3
上面的代码表示,ES6模块输入的变量是活的,完全能够反应出模块lib.js内部的变化。
最后,由于ES6输入的模块变量只是一个“符号链接”,所以这个变量是只读的,对它进行重新赋值,是会报错的。
如:
//lib.js
export let obj = {};
//main.js
import {obj} from './lib';
obj.prop = 123; //ok
obj = {}; //TypeError
上面的代码中,main从lib输入变量obj,可以对obj添加属性,但是重新赋值就会报错。
因为变量obj指向的地址是只读的,不能重新赋值,这样就好比main.js创建了一个名为obj的const变量。