前端模塊化是前端工程化的基石。時下,大前端時代中對模塊的運用更是無處不在,而放眼將來,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 執行完畢 執行完畢