JavaScrip模塊系統詳解

在這幾天的工做中,我須要調用同事編寫的兼容jQuery和React的通用組件。他爲了兼容jQuery風格的調用和React的組件化,分別export了一個default和幾個方法函數。在調用的過程當中,出現了一些小插曲:React代碼和老的jQuery老代碼調用時應該怎麼正確的import?雖然是很低級的問題,可是引起了我一些思考:export 和 import 與 module.exports 和 exports 之間的關係以及JavaScript模塊系統的發展歷程html

JavScript這門語言,在設計之初是沒有本身的模塊系統的。可是在 ES6 正式發佈以前,社區已經中已經出現了一些庫,實現了簡單的模塊風格,而且這種風格在 ES6 中也是適用的:node

  • 每一個模塊都是一段代碼,加載以後只會解析過程只會執行一次;jquery

  • 在模塊中能夠聲明變量,函數,類等;webpack

    • 默認狀況下,這些聲明都是這個模塊的局部聲明;es6

    • 能夠將一些聲明導出,讓其餘模塊引用;web

  • 一個模塊能夠經過模塊標識符或者文件路徑引入其餘模塊;express

  • 模塊都是單例的,即便屢次引用,也只有一個實例;編程

有一點要注意,避免經過global做爲來引用本身的模塊,由於global自己也是一個模塊。api

ES5中的模塊系統

前面說到的,在 ES6 以前,JavaScript 是沒有模塊系統這一說的。在社區的模塊風格出現以前,編寫 JavaScript經常會遇到這種狀況:數組

  • 全部的代碼寫在一個文件裏面,按照依賴順序,被依賴的方法必須寫在前面。 簡單粗暴,可是問題不少

    • 通用的代碼沒法重複利用。

    • 單個文件會愈來愈大,後期的命名也會愈來愈艱難。

  • 按照功能將代碼拆分紅不一樣文件,按照依賴順序加載,被依賴的方法必須先加載。通用代碼能夠複用,可是問題仍是不少

    • 過多全局變量,容易衝突。

    • 過多 JavaScript 腳本加載致使頁面阻塞(雖然 HTML5中的 defer和 async能夠適當的減輕這個問題)。

    • 過多依賴不方便管理和開發。

隨着 JavaScript 的地位慢慢提升,爲了知足平常開發的須要,社區中慢慢出現了相對比較贊成的模塊標準,主要有兩種:

  • CommonJS Modules: 這個標準主要在 Node.js 中實現(Node.js的模塊比 CommonJS 好稍微多一些特性)。其特色是:

    • 簡單的語法

    • 爲同步加載和服務端而設計

  • Asynchronous Module Definition (AMD): 這個標準最受歡迎的實現實在 RequireJS 中。其特色是:

    • 稍微複雜一點點的語法,使得AMD的運行不須要編譯

    • 爲異步加載和瀏覽器而設計

上述只是 ES5 模塊系統的簡單介紹,若是有興趣能夠去看看Writing Modular JavaScript With AMD, CommonJS & ES Harmony

CommonJS Modules 在 Node.js 中的實現

根據CommonJS規範,一個單獨的文件就是一個模塊。每個模塊都是一個單獨的做用域,在該模塊內部定義的變量,沒法被其餘模塊讀取,除非定義爲global對象的屬性,或者將屬性暴露出來。在 Nodejs就是如此。

好比:

const circle = require('./circle.js'); // 使用 require 加載模塊 circle
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);

在 circle.js 中:

const PI = Math.PI;
exports.area = (r) => PI * r * r;
exports.circumference = (r) => 2 * PI * r;

circle.js 模塊導出了 area()和 circumffference()兩個方法,變量 PI是這個模塊的私有變量。若是想爲自定義的模塊添加屬性或者方法,將它們添加到 exports 這個特殊的對象上就能夠達到目的。
若是但願模塊提供的接口是一個構造函數,或者輸出的是一個完整的對象而不是一個屬性,那麼可使用 module.exports 代替 exports。可是注意,exports 是 module.exports 的一個引用,只是爲了用起來方便,只要沒有重寫 module.exports對象,那麼module.exports.xxx就等價於exports.xxx

const square = require('./square.js');
var mySquare = square(2);
console.log(`The area of my square is ${mySquare.area()}`);

square.js:

module.exports = (width) => {
  return {
    area: () => width * width
  };
}

AMD規範

AMD是「Asynchronous Module Definition」的縮寫。經過異步方式加載模塊,模塊的加載不影響後續語句的執行,全部依賴加載中的模塊的語句,都會放在一個回調函數中,等到該模塊加載完成後,這個回調函數才運行。注意,在 AMD 中模塊名是全局做用域,能夠在全局引用。

AMD規範的API很是簡單:

define(id?, dependencies?, factory);

規範定義了一個define函數,它用來定義一個模塊。它包含三個參數,前兩個參數都是可選的。

  • id:是一個string字符串,它表示模塊的標識。一般用來定義這個模塊的名字,通常不用

  • dependencies:是一個數組,依賴的模塊的標識。也是可選的參數,每一個依賴的模塊的輸出將做爲參數一次傳入 factory 中。若是沒有指定 dependencies,那麼它的默認值是 ["require", "exports", "module"]。

  • factory:一個函數或者對象。若是是函數,在依賴的模塊加載成功後,會執行這個回調函數,它的返回值就是模塊的輸出接口或值。它的參數是全部依賴模塊的引用。

定義一個名爲 myModule 的模塊,它依賴 jQuery 模塊:

define('myModule', ['jquery'], function($) {
    // $ 是 jquery 模塊的輸出
    $('body').text('hello world');
});
// 使用
define(['myModule'], function(myModule) {});

ES6中的模塊系統

ES6 模塊系統的目標就是建立一個統一的模塊格式,讓 CommonJS 和 AMD的使用者都滿意:

  • 和CommonJS相似,可是更加簡潔的語法,循環引用的支持更好。

  • 和AMD相似,直接支持異步加載和可配置的模塊加載。

模塊標準主要有兩部分:

  1. 聲明語法:import 和 export

  2. 可編程的加載 API:配置模塊如何以及有條件地加載模塊

ES6模塊的基礎

在 ES6的模塊系統中,有兩種 export:命名的 export 和默認的 export。在一個文件中,命名的 export 能夠有多個,而默認的 default export 只能有一個。能夠同時使用,但最好仍是分開使用。

命名的export

也能夠在聲明表達式前面加上 export 關鍵字能夠直接導出將聲明的對象導出:

//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

若是要導出一個已經存在的變量,須要加上{}:

const random = Math.random;
export random;  // SyntaxError: Unexpected token, expected {
export { random };

使用 CommonJS 語法實現相同目的:

//------ lib.js ------
var sqrt = Math.sqrt;
function square(x) {
    return x * x;
}
function diag(x, y) {
    return sqrt(square(x) + square(y));
}
module.exports = {
    sqrt: sqrt,
    square: square,
    diag: diag,
};

//------ main.js ------
var square = require('lib').square;
var diag = require('lib').diag;
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

下面是來自 MDN 的更加完整的export 語法:

export { name1, name2, …, nameN };
export { variable1 as name1, variable2 as name2, …, nameN };
export let name1, name2, …, nameN; // also var
export let name1 = …, name2 = …, …, nameN; // also var, const

export expression;
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };

export * from …;
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
默認導出

每一個模塊只有一個默認導出的值, default export 能夠是一個函數,一個類,一個對象或者其餘任意值。有兩種形式的 default export:

  1. 被標記的聲明。導出一個函數或者類

  2. 直接導出值。導出表達式的運行結果

導出一個函數:

//------ myFunc.js ------
export default function () {} // 沒有分號 函數名無關緊要

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();

導出一個類:

//------ MyClass.js ------
export default class {} // 沒有分號 類名無關緊要

//------ main2.js ------
import MyClass from 'MyClass';
const inst = new MyClass();

導出表達式運行結果:

export default 'abc';
export default foo();
export default /^xyz$/;
export default 5 * 7;
export default { no: false, yes: true };

前面說的到的導出匿名函數和類,能夠將其視爲導出表達式的運行結果:

export default (function () {});
export default (class {});

每個 default export 都是這種結構:

export default <<expression>>

至關於:

const __default__  = <<expression>>;
export { __default__ as default }; // (A)

export後面是不能接變量聲明的,由於一個變量聲明表達式中能夠一次生命多個變量。考慮下面這種狀況:

export default const foo = 1, bar = 2, baz = 3; // not legal JavaScript!

應該導出 foo,bar,仍是 baz 呢?

必須在模塊的最頂層使用import和export
if (Math.random()) {
    import 'foo'; // SyntaxError
}

// You can’t even nest `import` and `export`
// inside a simple block:
{
    import 'foo'; // SyntaxError
}
import 會被提高到當前做用域的頂部

模塊的 import 會被提高到當前做用域的頂部。因此下面這種狀況是可行的:

foo();

import { foo } from 'my_module';
import 的一些細節

import的基本語法:

import defaultMember from "module-name";
import * as name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1 , member2 } from "module-name";
import { member1 , member2 as alias2 , [...] } from "module-name";
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as name from "module-name";
import "module-name";

對循環引用的支持

什麼是循環引用?模塊A 引用了模塊 B,模塊 B 又引用了模塊 A。若是可能的話,應該避免這種狀況出現,這會使得模塊之間過分的耦合。可是這種有時候又是沒法避免的。

CommonJS 中的循環引用

a.js 中的內容:

console.log('模塊 a 開始了!');
exports.done = false;
var b = require('./b.js');
console.log('在 a 中, b.done = %j', b.done);
exports.done = true;
console.log('模塊 a 結束了!');

b.js 中的內容:

console.log('模塊 b 開始了!');
exports.done = false;
var a = require('./a.js');
console.log('在 b 中, a.done = %j', a.done);
exports.done = true;
console.log('模塊 b 結束了!');

main.js 中的內容:

console.log('main 開始了!');
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main 中, a.done=%j, b.done=%j', a.done, b.done);

當 main.js 加載 a.js 時,a.js 又加載 b.js。這個時候,b.js 又會嘗試去加載 a.js 。爲了防止出現無限循環的加載,a.js 中的 exports 對象會返回一個 unfinished copy 給 b.js 模塊。而後模塊 b 完成加載,同時將提供模塊 a 的接口。當 main.js 加載完 a,b 兩個模塊以後,輸出以下:

main 開始了!
模塊 a 開始了!
模塊 b 開始了!
在 b 中, a.done = false
模塊 b 結束了!
在 a 中, b.done = true
模塊 a 結束了!
在 main 中, a.done=true, b.done=true

這種方式有其侷限性:

  • Nodejs風格的單個值的導出沒法工做。當a使用 module.exports 導出一個值時,那麼 b 模塊中引用的變量 a 在聲明以後就不會再更新

    module.exports = function(){};
  • 沒法直接命名你的引用

    var foo = require('a').foo; // foo  is undefined

ES6中的循環引用

ES6中,imports 是 exprts 的只讀視圖,直白一點就是,imports 都指向 exports 本來的數據,好比:

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4
// The imported value can’t be changed
counter++; // TypeError

所以在 ES6中處理循環引用特別簡單,看下面這段代碼:

//------ a.js ------
import {bar} from 'b'; // (i)
export function foo() {
  bar(); // (ii)
}

//------ b.js ------
import {foo} from 'a'; // (iii)
export function bar() {
  if (Math.random()) {
    foo(); // (iv)
  }
}

假設先加載模塊 a,在模塊 a 加載完成以後,bar 間接性地指向的是模塊 b 中的 bar。不管是加載命令的 imports 仍是未完成的 imports,imports 和 exports 之間都有一個間接的聯繫,因此老是能夠正常工做。

ES6 模塊加載器 API

除了聲明式加載模塊,ES6還提供了一個可編程的 API:

  • 以編程的方式使用模塊

  • 配置模塊的加載

要注意,這個 API 並非ES6標準中的一部分,在「JavaScript Loader Standrad」中,而且具體的標準還在制定中,因此下面講到的內容都是試驗性的。

Loaders 的簡單使用

Loader 用於處理模塊標識符和加載模塊等。它的 construct 是Reflect.Loader。每一個平臺在全局做用域中都有一個全局變量System的實例來實現 loader 的一些特性。

你能夠經過 API 提供的 Promise,以編碼的方式 import 一個模塊:

System.import('some_module')
.then(some_module => {
    // Use some_module
})
.catch(error => {
    ···
});

System.import() 能夠:

  • 能夠在 script 標籤中使用模塊

  • 有條件地加載模塊

System.import() 返回一個模塊, 能夠用 Promise.all() 來導入多個模塊:

Promise.all(
    ['module1', 'module2', 'module3']
    .map(x => System.import(x)))
.then(([module1, module2, module3]) => {
    // Use module1, module2, module3
});
Loader的其餘方法

Loader 還有一些其餘方法,最重要的三個是:

  • System.module(source, [options])
    將 source 中的 JavaScript 代碼當作一個模塊執行,返回一個 Promise

  • System.set(name, modules)
    註冊一個模塊,好比用 System.module 建立的模塊

  • System.define(name, source, [options])
    執行 source 中的代碼,將返回的結果註冊爲一個模塊

目前 Loader API 還處於試驗階段,更多的細節不想在深刻。有興趣的話能夠去看看

模塊導入的細節

在 CommonJS 和 ES6中,兩種模塊導入方式有一些不一樣:

  • 在 CommonJS 中,導入的內容是模塊導出的內容的拷貝。

  • 在 ES6 中,導出值得實時只讀視圖,相似於引用。

在 CommonJS 中,若是你將一個導入的值保存到一個變量中,這個值會被複制兩次:第一次是這個值所屬模塊導出時(行 A),第二次是這個值被引用時(行 B)。

//------ lib.js ------
var counter = 3;
function incCounter() {
    counter++;
}
module.exports = {
    counter: counter, // (A)
    incCounter: incCounter,
};

//------ main1.js ------
var counter = require('./lib').counter; // (B)
var incCounter = require('./lib').incCounter;

// The imported value is a (disconnected) copy of a copy
console.log(counter); // 3
incCounter();
console.log(counter); // 3

// The imported value can be changed
counter++;
console.log(counter); // 4

若是經過 exports對象來訪問這個值,這個值仍是會再複製一次:

//------ main2.js ------
var lib = require('./lib');

// The imported value is a (disconnected) copy
console.log(lib.counter); // 3
lib.incCounter();
console.log(lib.counter); // 3

// The imported value can be changed
lib.counter++;
console.log(lib.counter); // 4

和 CommonJS 不一樣的是,在 ES6中,全部的導入的數據都是導出值的視圖,每個導入的數據都和原始的數據有一個實時鏈接(並非 JS 中Object引用的那種概念,由於導出的值能夠是一個原始類型,primitive type,並且導入的數據是隻讀的)。

  • 無條件的引入 (import x from 'foo') 就是用 const 聲明的變量

  • 模塊的屬性foo (import * as foo from 'foo') 則是建立一個 frozen object.

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main1.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

// The imported value can’t be changed
counter++; // TypeError

若是使用*引入模塊,會獲得相同的結果:

//------ main2.js ------
import * as lib from './lib';

// 導入的值 counter 是活動的
console.log(lib.counter); // 3
lib.incCounter();
console.log(lib.counter); // 4

// 導入的值是隻讀的不能被修改
lib.counter++; // TypeError

雖然不能修改導入的值,可是能夠修改對象指向的內容,這個 const 常量的處理是一致的。例如:

//------ lib.js ------
export let obj = {};

//------ main.js ------
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError

結束

關於更多 ES6 模塊相關的內容,有興趣的朋友能夠去下面這些地方看看:

參考資料:

  1. http://stackoverflow.com/a/40295288

  2. http://exploringjs.com/es6/ch_modules.html

  3. http://zhaoda.net/webpack-handbook/amd.html

  4. [https://nodejs.org/api/module...

  5. http://speakingjs.com/es5/ch17.html#freezing_objects

相關文章
相關標籤/搜索