前端模块化是前端工程化的基石。时下,大前端时代中对模块的运用更是无处不在,而放眼将来,es6中所提出的import和export的形式彷佛将统一先后端的模块化加载。
在学习ES6的模块化以前先复习一下以前出现的模块化,比较经常使用的有三种规范定义:CommonJS、AMD、CMD。前端
它们的特色与相互间的区别是:es6
var clock = require('clock.js') clock.start();
上例表示,clock
的调用必须等待clock.js
请求加载成功,换句话说,是同步操做,而这也致使了CommonJS普遍应用于服务端而不是客户端(服务器读取模块都是在本地磁盘,加载速度快,而若是是在客户端则容易出现‘假死’状态)那么能不能用异步加载模块呢?express
require([module],callback); // eg require(['clock.js'],function(clock){ clock.start(); })
虽然实现了异步加载,规避了浏览器的“假死”问题,可是也存在缺点: 一开始就把全部依赖写出来是不符合逻辑顺序的。那么,能不能像CommonJS同样用的时候才require,而后还能支持异步加载后执行呢?后端
define(function(require,exports,module){ var clock = require('clock.js'); clock.start(); })
AMD和CMD的区别是对依赖模块的执行时机不一样,而不是加载处理方式不一样,两者皆为异步加载模块。 前端工程化
AMD依赖前置,js能够方便地清楚依赖模块有哪些,当即加载;浏览器
CMD就近依赖,开发者能够在须要用到依赖的时候再require,可是对于js处理器来讲,须要把代码处理为字符串解析一遍才知道依赖了哪些模块,即牺牲性能来得到开发的便利,虽然实际上解析的时间短到能够忽略,可是也有不少人诟病CMD这一点。缓存
ES6的模块化设计思想是尽可能静态化,使得编译时就能肯定模块的依赖关系。服务器
对比CommonJS和ES6模块:app
// CommonJS let { start, exists, readFile } = require('fs') // 至关于 // let _fs = require('fs') // let start = _fs.start, exists = _fs.exists, readFile = _fs.readFile // ES6 import { start, exists, readFile } from 'fs'
上述例子中,CommonJS的实质是总体加载fs模块生成一个_fs
对象,以后再从对象中分别读取3个方法,称为“运行时加载”。而ES6模块是加载3个方法,称为“编译时加载”dom
在ES6模块中自动采用严格模式。规定:
with
delete
不可删除属性直接报错delete prop
、只能删除属性delete global[prop]
eval
不会再外层做用域引入变量eval
和arguments
不可从新赋值arguments
不会自动反应函数参数变化this
指向全局注意:在ES6模块中,顶层this
为undefined
,不该该被使用。
第一种:
export var a = '123'; export const _b = '2323' export let c = '2222'
第二种:
var a = '123'; const _b = '2323' let c = '2222' export {a, _b, c}; // 推荐
第三种(第二种基础上加上as关键词重命名)
var a = '123'; const _b = '2323' let c = '2222' export { a as stream1, _b as stream2, c as stream3};
注意:
- export语句输出的接口是对应值的引用,也就是一种动态绑定关系,经过该接口能够获取模块内部实时的值。
对比CommonJS规范:CommonJS模块输出的是值的缓存,不存在动态更新。
- export命令规定要处于模块顶层,不过出如今块级做用域内,就会报错,import同理。
第一种:
import {a, _b ,c} from './profile'
第二种:
import {stream1 as firstVal} from './profile'
import 是静态执行,不能够应用表达式、变量和if结构。
if(x == 1){ import { foo } from 'module1' }else{ //... }
import语句是Singleton模式:虽然foo
和bar
在两个语句中加载,可是对应的是同一个my_module
实例。
import { foo } from './module1' import { bar } from './module1' // 至关于 import {foo,bar} from './module1'
可使用*来指定一个对象,全部输出值都加载到这个对象上:
import * as circle from './module1' circle.foo(); circle.bar();
因为模块总体加载所在的对象都是能够静态分析的,因此不容许运行时改变。
import * as circle from './module1' // 下面两行都是不容许的 circle.foo = 123; circle.bar = function(){}
export default命令能够为模块默认输出
// module2.js export default function(){ console.log('123') } // 至关于 function a(){ console.log('123') } export {a as default};
import命令能够为匿名函数指定任意名字
import defaultFn from './module2' // 至关于 import {default as defaultFn} from './module2'
export { foo, bar} from 'my_module'; // 等同于 import {foo,bar} from 'my_module'; export{foo,bar};
export {es6 as default} from './someModule' // 等同于 import {es6} from './someModule' export default es6;
前面提到过,require是动态加载,便可以在用的时候再require;而import是静态执行,只能处于代码最顶层,不能够存在于块级做用域中。这致使import没法在运行中执行(相似于AMD的缺点)。
因而就有了一种提案:引入import()函数,相似于Node的require函数(CommonJS),可是它实现了异步加载。
定义:import()函数接收与import相同的参数,返回一个Promise对象,加载获取到的值做为then方法的回调参数。
const main = document.querySelector('main') import(`./section-modules/${someVariable}.js`) .then(module => { module.loadPageInto(main); }) .catch(err => { main.textContext = err.message; })
// 加载得到接口参数: import('./module1.js') .then(({default:defaultFn,foo,bar}) => { console.log(defaultFn) })
// 同时加载多个模块并应用于async函数中 async function main() { const myModule = await import('./myModule.js'); const {export1, export2} = await import('./myModule.js'); const [module1, module2, module3] = await Promise.all([ import('./module1,js'), import('./module2.js'), import('./module3.js') ]) } main();
使用import命令加载CommonJS模块,Node会自动将module.exports属性当作模块的默认输出,即等同于export default
// a.js module.exports = { foo: 'hello', bar: 'world' } // 在import引入时等同于 export default { foo: 'hello', bar: 'world' }
CommonJs模块是运行时肯定输出接口,因此采用import命令加载CommonJS模块时,只能使用总体输入(*)。
import {readfile} from 'fs' //当'fs'为CommonJS模块时错误 // 总体输入 import * as express from 'express' const app = express.default();
require命令加载ES6模块时,全部的输出接口都会成为输入对象的属性。
// es.js let foo = {bar : 'my-default'}; exxport default foo; foo = null; // cjs.js const es_namespace = require('./es') console.log(es_namespace.default);// {bar:'my-default'}
有了新欢也不能忘了旧爱,让咱们再来继续对比CommonJS和ES6模块化的区别,进一步体会理解ES6模块化的特性。
CommonJS模块输出的是一个值的复制,ES6输出的是值的引用
// lib.js let num = 3; function changeNum() { num = 4; } module.exports = { num: num, changeNum: changeNum, }; //main.js var mod = require('./lib.js') console.log(mod.num); // 3 mod.changeNum(); console.log(mod.num); // 3
这是因为,mod.num是一个原始类型的值,会被缓存。能够经过写成一个函数,来获得内部修改后的值:
// lib.js let num = 3; function changeNum() { num = 4; } module.exports = { get num(){ return num }, changeNum: changeNum, }; //main.js var mod = require('./lib.js') console.log(mod.num); // 3 mod.changeNum(); console.log(mod.num); // 3
对比ES6模块:
// lib.js export let num = 3; export function changeNum() { num = 4; } //main.js import {num,changeNum} from './lib.js' console.log(num); // 3 changeNum(); console.log(num); // 4
CommonJS一个模块对应一个脚本文件,require命令每次加载一个模块就会执行整个脚本,而后生成一个对象。这个对象一旦生成,之后再次执行相同的require命令都会直接到缓存中取值。也就是说:CommonJS模块不管加载多少次,都只会在第一次加载时运行一次,之后再加载时就返回第一次运行的结果,除非手动清除系统缓存。
// a.js exports.done = false; var b = require('./b.js'); // 1. a.js暂停执行,转到执行b.js ; b.js完毕后回来,b:{done:true} console.log('在a.js中,b.done=%j',b.done); // 5. '在a.js中,b.done=true' exports.done = true; console.log('a.js执行完毕') // 6. 'a.js执行完毕' // b.js exports.done = false; var a = require('./b.js') // 2. a:{done:false} console.log('在b.js中,a.done=%j',a.done); // 3. '在b.js中,a.done=false' exports.done = true; console.log('b.js执行完毕') // 4. 'b.js执行完毕',继续执行a.js // main.js var a = require('./a.js'); var b = require('./b.js'); console.log('在main.js中,a.done=%j,b.done=%j',a.done,b.done); // 7.'在main.js中,a.done=true,b.done=true'
上面代码能够看到:第一,在b.js中,a.js没有执行完毕,第二,当main.js执行到第二行时不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行:exports.done = true
总结一下:
对比:ES6模块是动态引用,变量不会被缓存
// a.js import {bar} from './b.js'; export function foo(){ console.log('foo') bar(); console.log('执行完毕') } foo(); // b.js import {foo} from './a.js' // 若是为CommonJS,这里直接就返回undefined值且不会再更改 export function bar(){ console.log('bar') if(Math.random() > 0.5){ foo(); } } // 执行结果可能为:foo bar 执行完毕 // 执行结果也可能为: foo bar foo bar 执行完毕 执行完毕