JS 做爲一名編程語言,一直以來沒有模塊的概念。嚴重致使大型項目開發受阻,js 文件越寫越大,不方便維護。其餘語言都有模塊的接口,好比 Ruby 的 require,python 的 import,C++ 天生的 #include,甚至 CSS 都有 @import。在 ES6 以前,有主要的2個模塊化方案:CommonJS 和 AMD。前者用於服務器,後者用於瀏覽器。CommonJS 這樣引入模塊:python
let {stat, exists, readFile} = require('fs');
AMD 和 CommonJS 引入模塊方法差很少,其表明是 require.js。這裏咱們主要研究 ES6 提供的方法:編程
import {stat, exists, readFile} from 'fs'
這個方法相比以前的方案,具備如下優勢:json
模塊功能主要由2個命令組成:export 和 import。export 關鍵字用於規定模塊的對外接口,import 關鍵字用於輸入其餘模塊提供的功能。這裏須要知道的是,ES6 中模塊導出的都會構成一個對象。segmentfault
export var a = 1; export var b = 2; export var c = 3;
上面導出了3個變量,和下面的下法等價:瀏覽器
var a = 1; var b = 2; var c = 3; export {a, b, c}; //這種寫法更好,在文件結尾統一導出,清晰明瞭
固然還能夠導出函數和類緩存
//導出一個函數 add export function add(x,y){ return x + y; } //導出一個類 export default class Person{}
還能夠在導出時候對參數重命名:服務器
function foo(){} function bar(){} export {foo, bar as bar2, bar as bar3} //bar 被重命名爲 bar2,bar3輸出了2次
// abc.js var a = 1; var b = 2; var c = 3; export {a, b, c} //main.js import {a, b, c} from './abc'; //接受的變量用大括號表示,以解構賦值的形式獲取 console.log(a, b, c);
導入的時候也能夠爲變量從新取一個名字編程語言
import {a as aa, b, c}; console.log(aa, b, c)
若是想在一個模塊中先輸入後輸出同一個模塊,import語句能夠和export語句寫在一塊兒。模塊化
// 正常寫法 import {a, b, c} form './abc'; export {a, b, c} // 使用簡寫, 可讀性很差,不建議 export {a, b, c} from './abc'; //ES7 提議,在簡化先輸入後輸出的寫法。如今不能使用,也不建議使用,可讀性很差 export a, b, c from './abc'
使用 import 和 export 須要注意一下幾個方面:函數
// foo.js export var foo = 'foo'; setTimeout(function() { foo = 'foo2'; }, 500); // main.js import * as m from './foo'; console.log(m.foo); // foo setTimeout(() => console.log(m.foo), 500); //foo2 500ms 後一樣會被修改
import './foo'; //執行 foo.js 但不引入任何值
固然模塊能夠做爲總體加載,使用*關鍵字,並利用 as 重命名獲得一個對象,全部得到的 export 的函數、值和類都是該對象的方法:
// abc.js export var a = 1; export var b = 2; export var c = 3; // main.js import * as abc from './abc'; console.log(abc.a, abc.b, abc.c);
上面 main.js 中的總體加載能夠用 module 關鍵字實現:
//暫時沒法實現 module abc from './abc'; console.log(abc.a, abc.b, abc.c); //1 2 3
注意,以上2種方式得到的接口,不包括 export default 定義的默認接口。
爲了使模塊的用戶能夠不看文檔,或者少看文檔,輸出模塊的時候利用 export default 指定默認輸出的接口。使用 export defalut 輸出時,不須要大括號,而 import 輸入變量時,也不須要大括號(沒有大括號即表示得到默認輸出)
// abc.js var a = 1, b = 2, c = 3; export {a, b}; export default c; //等價於 export default 3; // main.js import {a, b} from './abc'; import num from './abc'; // 不須要大括號, 並且能夠直接更名(若是必須用原名不還得看手冊麼?) console.log(a, b, num) // 1 2 3
本質上,export default輸出的是一個叫作default的變量或方法,輸入這個default變量時不須要大括號。
// abc.js var a = 20; export {a as default}; // main.js import a from './abc'; // 這樣也是能夠的 console.log(a); // 20 // 這樣也是能夠的 import {default as aa} from './abc'; console.log(aa); // 20
若是須要同時輸入默認方法和其餘變量能夠這樣寫 import:
import customNameAsDefaultExport, {otherMethod}, from './export-default';
這裏須要注意:一個模塊只能有一個默認輸出,因此 export default 只能用一次
所謂模塊的繼承,就是一個模塊 B 輸出了模塊 A 所有的接口,就彷彿是 B 繼承了 A。利用 export *
實現:
// circleplus.js export * from 'circle'; //固然,這裏也能夠選擇只繼承其部分接口,甚至能夠對接口更名 export var e = 2.71828182846; export default function(x){ //從新定義了默認輸出,若是不想從新定義能夠:export customNameAsDefaultExport from 'circle'; return Math.exp(x); } //main.js import * from 'circleplus'; //加載所有接口 import exp from 'circleplus'; //加載默認接口 //...use module here
上面這個例子 circleplus 繼承了 circle。值得一提的是,export *
不會再次輸出 circle 中的默認輸出(export default)。
在使用和定義模塊時,但願能夠作到如下幾個建議:
ES6 模塊加載的機制是值的應用,而 CommonJS 是值的拷貝。這意味着, ES6 模塊內的值的變換會影響模塊外對應的值,而 CommonJS 不會。 ES6 遇到 import 時不會馬上執行這個模塊,只生成一個動態引用,須要用的時候再去裏面找值。有點像 Unix 中的符號連接。因此說 ES6的模塊是動態引用,不會緩存值。以前的這個例子就能夠說明問題:
// foo.js export let counter = 3; export function inc(){ counter++; } // main.js import {counter, inc} from './foo'; console.log(counter); //3 inc(); console.log(counter); //4
咱們看一個 CommonJS 的狀況
// foo.js let counter = 3; function inc(){ counter++; } module.exports = { counter: counter, inc: inc } // main.js let foo = require('./foo') let counter = foo.counter; let inc = foo.inc; console.log(counter); //3 inc(); console.log(counter); //3
不知道大家只不知道循環引用,在內存管理與垃圾回收中提到過:若是 A 對象的一個屬性值是 B 對象,而 B 對象的一個屬性值是 A 對象,就會造成循環引用,沒法釋放他們的內存。而模塊中也會出現循環加載的狀況:若是 A 模塊的執行依賴 B 模塊,而 B 模塊的執行依賴 A 模塊,就造成了一個循環加載,結果程序不能工做,或者死機。然而,這樣的關係很難避免,由於開發者衆多,誰都會在開發本身的模塊時使用別人的幾個模塊,長此以往,就行互聯網同樣,這樣的依賴也織成了一個網。
ES6 和 CommonJS 處理循環加載又不同,從 CommonJS 開始研究
CommonJS 每次執行完一個模塊對應的 js 文件後在內存中就生成一個對象:
{ id: '...', //表示屬性的模塊名 exports: {...}; //模塊輸出的各個接口 loaded: true, //表示是否加載完畢 //...內容不少,不一一列舉了 }
以後使用這個模塊,即便在寫一遍 requrie,都不會再執行對應 js 文件了,會直接在這個對象中取值。
CommonJS 若是遇到循環加載,就輸出已執行的部分,以後的再也不執行,執行順序以註釋序號爲準(從0開始):
// a.js exports.done = false; //1. 先輸出 done var b = require('./b.js'); //2. 進入 b.js 執行 b.js //5. 發現 a.js 沒執行完,那就重複不執行 a.js,返回已經執行的 exports console.log(`In a.js, b.done = ${b.done}`); //10. 第2步的 b.js 執行完了,繼續執行 a.js 獲得控制檯輸出:'In a.js, b.done = true' exports.done = true; //11 console.log('a.js executed'); //12. 獲得控制檯輸出:"a.js executed" // b.js exports.done = false; //3. 先輸出 done var a = require('./a.js'); //4. 執行到這裏發生循環加載,去 a.js 執行 a.js //6. 只獲得了 a.js 中的 done 爲 false console.log(`In b.js, a.done = ${a.done}`); //7. 獲得控制檯輸出:"In b.js, a.done = false" exports.done = true; //8. 輸出 done, 覆蓋了第3步的輸出 console.log('b.js executed'); //9. 獲得控制檯輸出:"b.js executed" //main.js var a = require("./a.js"); //0. 去 a.js 執行 a.js var b = require("./b.js"); //13. b.js 已經執行過了,直接去內存中的對象取值 console.log(`In main,a.done = ${a.done}, b.done = ${b.done}`) //獲得控制檯輸出:'In main,a.done = true, b.done = true'
因爲 ES6 使用的是動態引用,遇到 import 時不會執行模塊。因此和 CommonJS 有本質的區別。一樣咱們看個例子:
// a.js import {bar} from './b.js'; export function foo(){ bar(); console.log("finished") } // b.js import {foo} from './a.js'; export function bar(){ foo(); } //main.js import * from './a.js'; import * from './b.js'; //...
上面這段代碼寫成 CommonJS 形式是沒法執行的,應爲 a 輸出到 b 的接口爲空(null), 因此在 b 中調用 foo() 要報錯的。可是 ES6 能夠執行,獲得控制檯輸出"finished"
另外一個例子是這樣的。執行順序以註釋序號爲準(從0開始):
// even.js import {odd} from './odd'; //2. 獲得 odd.js 動態引用,但不執行 export var counter = 0; //3. 輸出 counter 的引用 export function even(n){ //4. 輸出 even 函數的引用 counter++; //6 return n === 0 || odd(n - 1); //7. n 不是 0, 去 odd.js 找 odd() 函數 //10. 執行 odd 函數,傳入9 } // odd.js import {even} from './even'; //8. 獲得 even.js 動態引用,但不執行 export function odd(n){ //9. 輸出 odd 函數 return n !== 0 && even(n - 1); //11. 回到第2步,找到 even 函數,回來執行,傳入8,直到 n 爲 0 結束 } // main.js import * as m from './even'; //0. 獲得 even.js 動態引用,但不執行 console.log(m.even(10)); //1. 去 even.js 找 even 函數。 //5. 執行函數,傳入10 //最終獲得控制檯輸出:true console.log(m.counter); //因爲 ES6 模塊傳值是動態綁定的(下同),因此獲得控制檯輸出:6 console.log(m.even(20)); //分析同上,獲得控制檯輸出:true console.log(m.counter); //獲得控制檯輸出:17
上面寫了11步,以後是一個循環,沒有繼續寫。但不難看出 ES6 根本不怕循環引用,只要模塊文件的動態引用在,就能夠計算完成。不過,別看這個過程比 CommonJS 複雜,每次都有從新運行模塊文件,而不直接讀取緩存,但 ES6 的這些工做在編譯期間就完成了,比 CommonJS 在運行時間處理模塊要效率更高,體驗更好。