Javascript模塊化開發基礎

認識模塊

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

  • 最大的優勢就是編譯的時候完成模塊加載,稱之爲"編譯時加載", 而 CommonJS 使用的是 "運行時加載"。明顯 ES6 效率更高
  • 再也不須要 UMD 模塊格式,將來服務器和瀏覽器必定都能支持這種方法
  • 未來瀏覽器 API 能夠用模塊的格式提供,不須要作成全局變量或 navigator 的屬性
  • 不須要反覆的封裝和定義命名空間,直接以模塊形式提供便可
  • 模塊默認工做在嚴格模式,即便沒有指定"use strict", 關於嚴格模式能夠看:Javascript基礎(2) - 嚴格模式特色
  • 一個模塊就是一個文件,有效地減小了全局變量污染

export 和 import

模塊功能主要由2個命令組成:export 和 import。export 關鍵字用於規定模塊的對外接口,import 關鍵字用於輸入其餘模塊提供的功能。這裏須要知道的是,ES6 中模塊導出的都會構成一個對象。segmentfault

  • export 導出模塊的部分方法屬性或類
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次
  • import 導入命令能夠導入其餘模塊經過 export 導出的部分
// 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 須要注意一下幾個方面:函數

  • export 必須寫在所在模塊做用於的頂層。若是寫在了內部做用於會報錯
  • 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 具備聲明提高,並且會提高到整個文件最上面
  • import 得到的變量都是隻讀的,修改它們會報錯
  • 在 export 輸出內容時,若是同時輸出多個變量,須要使用大括號{},同時 import 導入多個變量也須要大括號
  • import 引入模塊的默認後綴是 .js, 因此寫的時候能夠忽略 js 文件擴展名
  • import 會執行要所加載的模塊。以下寫法僅僅執行一個模塊,不引入任何值
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 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)。

在使用和定義模塊時,但願能夠作到如下幾個建議:

  • Module 語法是 JavaScript 模塊的標準寫法,堅持使用這種寫法。使用 import 取代 require, 使用 export 取代module.exports
  • 若是模塊只有一個輸出值,就使用 export default,若是模塊有多個輸出值,就不使用 export default
  • 儘可能不要 export default 與普通的 export 同時使用
  • 不要在模塊輸入中使用通配符。由於這樣能夠確保你的模塊之中,有一個默認輸出(export default)
  • 若是模塊默認輸出一個函數,函數名的首字母應該小寫;若是模塊默認輸出一個對象,對象名的首字母應該大寫

ES6 模塊加載的實質

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

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

因爲 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 在運行時間處理模塊要效率更高,體驗更好。

相關文章
相關標籤/搜索