深刻理解Javascript之Module

什麼是模塊

模塊(module)是什麼呢? 模塊是爲了軟件封裝,複用。當今開源運動盛行,咱們能夠很方便地使用別人編寫好的模塊,而不用本身從頭開始編寫。在程序設計中,咱們一直強調避免重複造輪子(Don't Repeat Yourself,DRY)。javascript

想象一下,沒有模塊的日子,第三庫基本都是導出一個全局變量供開發者使用。例如jQuery$lodash_。這些庫已經儘可能避免了全局變量衝突,只使用幾個全局變量。可是仍是不能避免有衝突,jQuery還提供了noConflict。更遑論咱們本身編寫的代碼。html

最初,Javascript 中是沒有模塊的概念的。這可能與一開始 Javascript 的定位有關。Javascript 最初只是但願給網頁增長動態元素,定位是簡單易用的腳本。 可是,隨着網頁端功能愈來愈豐富,程序愈來愈龐大,軟件變得愈來愈難以維護。特別是隨着 NodeJs 的興起,Javascript 語言進入服務端編程領域。在編寫大型複雜的程序,模塊更是必須品。java

模塊只是一個抽象概念,要想在實際編程中使用還須要規範。若是沒有規範,我有這種寫法,你用那種寫法,豈不是亂了套。node

目前,模塊的規範主要有3中,CommonJS模塊AMD模塊和ES6模塊。本文着重講解 CommonJS 模塊(以 Node 實現爲表明)和ES6模塊。git

2.CommonJS模塊

CommonJS 實際上是一個通用的 Javascript 語言規範,並不只僅是模塊的規範。Node 中的模塊遵循 CommonJS 標準。es6

基本用法

Node 中提供了一個require方法用來加載模塊。例如:github

var fs = require('fs');

fs.readFile('file1.txt', 'utf8', function (err, data) {
    if (err) {
        console.error(err);
    } else {
        console.log(data);
    }
});
複製代碼

導入模塊以後就可使用模塊中定義的接口了,如上例中的readFile編程

模塊類別

在 Node 中大致上有3種模塊,普通模塊、核心模塊和第三方模塊。 普通模塊與核心模塊的導入方式稍微有些區別。普通模塊是咱們本身編寫的模塊,核心模塊是 Node 提供的模塊。上面咱們使用的fs就是核心模塊。導入普通模塊時,須要在require的參數中指定相對路徑。例如:api

var myModule = require('./myModule');
myModule.func1();
複製代碼

模塊myModule的後綴.js後綴能夠省略。框架

Node 將核心模塊編譯進了引擎。導入核心模塊只須要指定模塊名,Node 引擎直接查找核心模塊字典。

第三方模塊的導入也是指定模塊名,可是模塊的查找方式有所不一樣。 首先,在項目目錄下的node_modules目錄中查找。 若是沒有找到,接着去項目目錄的父目錄中查找。 直到找到加載該模塊,或者到根目錄還未找到返回失敗。

定義模塊

在咱們平常的編程中,常常須要將一些功能封裝在一個模塊中,方便本身或他人使用。在 Node 中定義模塊的語法很簡單。模塊單獨在一個文件中,文件中可使用exports導出接口或變量。例如:

function addTwoNumber(a, b) {
    return a + b;
}

exports.addTwoNumber = addTwoNumber;
複製代碼

假設該模塊在文件myMath.js中。在同一目錄下,咱們能夠這樣來使用:

var myMath = require('./myMath');

console.log(myMath.addTwoNumber(10, 20)); // 30
複製代碼

模塊導出詳解

函數具體是怎麼導出的呢?除了exports,咱們常常看到的module.exports__dirname__filename是從哪裏來的? 在執行require函數的時候,咱們能夠理解 Node 額外作了一些處理。

  • 首先,將模塊所在文件內容讀出來。而後將這些內容包裹在一個函數中:
function _doRequire(module, exports, __filename, __dirname) {
    // 模塊文件內容
}
複製代碼
  • 接下來,Node 引擎構造一個空的模塊對象,給這個對象一個空的exports屬性,而後推算出__filename(當前導入的這個模塊的全路徑文件名)和__dirname(模塊文件所在路徑):
var module = {};
module.exports = {}
// __filename = ...
// __dirname = ...
複製代碼
  • 而後,調用第一步構造的那個函數,傳入參數:
_doRequire(module, module.exports, __filename, __dirname);
複製代碼
  • 最後require返回的是module.exports的值。

按照上面的過程,咱們能夠很清楚地理解模塊的導出過程。而且也能很快地判斷一些寫法是否有問題:

錯誤寫法:

function addTwoNumber(a, b) {
    return a + b;
}

exports = {
    addTwoNumber: addTwoNumber;
}
複製代碼

這種寫法爲何不對?exports實際上初始時是module.exports的一個引用。給exports賦一個新值後,module.exports並無改變,仍是指向空對象。最後返回的對象是module.exports,沒有addTwoNumber接口。

正確寫法:

function addTwoNumber(a, b) {
    return a + b;
}

// 正確寫法一
exports.addTwoNumber = addTwoNumber;

// 正確寫法二
module.exports.addTwoNumber = addTwoNumber;

// 正確寫法三
module.exports = {
    addTwoNumber: addTwoNumber
};
複製代碼

exportsmodule.exports開始指向的是同一個對象。寫法一經過exports設置屬性,一樣對module.exports也可見。寫法二經過module.exports設置屬性也能夠導出。 寫法三直接設置module.exports就更不用說了。

建議在程序開發中,堅持一種寫法。我的以爲寫法三顯示設置相對較容易理解。

**有一點須要注意:不是隻有對象能夠導出,函數、類等值也能夠。**例以下面就導出了一個函數:

function addTwoNumber(a, b) {
    return a + b;
}

module.exports = addTwoNumber;
複製代碼

3.ES模塊

ES6 在標準層面爲 Javascript 引入了一套簡單的模塊系統。ES6 模塊徹底能夠取代 CommonJS 和 AMD 規範。當前熱門的開源框架 React 和 Vue 都已經使用了 ES6 模塊來開發。

基本使用

ES6 模塊使用export導出接口,import from導入須要使用的接口:

// myMath.js
export var pi = 3.14;

export function addTwoNumber(a, b) {
    return a + b;
}

// 或
var pi = 3.14;

function addTwoNumber(a, b) {
    return a + b;
}

export { pi, addTwoNumber };
複製代碼
// main.js
import { addTwoNumber } from './myMath';

console.log(addTwoNumber(10, 20));
複製代碼

myMath.js中經過export導出一個變量pi和一個函數addTwoNumber。上例中演示了兩種導出方式。一種是一個個導出,對每個須要導出的接口都應用一次export。第二種是在文件中某處集中導出。固然,也能夠混合使用這兩種方式。推薦使用第二種導出方式,由於能在一處比較清楚的看出模塊導出了哪些接口。

ES6 模塊特性

ES6 模塊有一些須要瞭解和注意的特性。

靜態加載

ES6 模塊最重要的特性是「靜態加載」,導入的接口是隻讀的,不能修改。NodeJS 中的模塊,是動態加載的。

靜態加載就是「編譯」時就已經肯定了模塊導出,能夠作到高效率,而且便於作靜態代碼分析。同時,靜態加載也限制了模塊的加載在文件中全部語句以前,而且導入語法中不能含有動態的語法結構(例如變量、if語句等)。

例如:

// 能夠調用,由於模塊加載是「編譯」時進行的。
funcA();
import { funcA, funcB } from './myModule';

// 錯誤,導入語法中含有變量
var foo = './myModule';
import { funcA, funcB } from './myModule';

// 錯誤,在if語句中
if (foo == "myModule") {
    import { funcA, funcB } from './myModule';
} else {
    import { funcA, funcB } from './hisModule';
}


// 錯誤,導出的接口是隻讀的,不能修改
import { funcA, funcB } from './myModule';
funcA = function () {};
複製代碼

導出的接口與模塊中定義的變量或函數必須是一一對應的。並且模塊內相應的值修改了,外部也能感知到。看下面代碼:

// 錯誤,導出值1,模塊中沒有對應
export 1;

// 錯誤,實際上也是導出1,模塊中沒有對應
var m = 1;
export m;

// 能夠這樣來導出,導出的m與模塊中的變量m對應
export var m = 1;

// 能夠這樣導出
var m = 1;
export {m};
複製代碼
var foo = "bar";
setTimeout(2000, () => { foo = "baz"});

// 2s後foo變爲"baz",外部能感知到
複製代碼
別名

在導出模塊時,能夠爲接口指定一個別名。這樣,後續能夠修改內部接口而保持導出接口不變。例如:

// myModule.js
var funcA = function () {
}

var funcB = function () {
}

export {
    funcA as func1,
    funcB as func2,
    funcB as myFunc,
}
複製代碼

上面咱們導出以別名func1導出函數funcA,以別名func2myFunc導出函數funcBfunc2myFunc都是指向同一個函數funcB的。下面看看使用這個模塊:

// main.js
import { func1, func2, myFunc } from './myModule';
複製代碼

一樣的,導入模塊時也能夠指定別名:

// main.js
import { func1 as func } from './myModule';
複製代碼
default導出

上面介紹的模塊導入必須知道接口名字。有時候,用戶學習一個模塊時但願可以快速上手,不想去看文檔(怎麼會有這個懶的人🤣)。ES6 提供了default導出。例如:

// myModule.js
export default function () {
    console.log('hi');
}

// default導出方式能夠看作是導出了一個別名爲default的接口
var f = function () {
    console.log('hi');
}
export { f as default };
複製代碼

在外部導入的時候,須要省略花括號:

// main.js
import func from './myModule';
func();
複製代碼

也能夠兩種方式,同時使用:

// myModule.js
function foo() {
    console.log('foo');
}

export default foo;

function bar() {
    console.log('bar');
}

export { bar };
複製代碼
// main.js
import foo, { bar } from './myModule';
複製代碼
總體加載

ES6 還容許一種總體加載的方式導入模塊。經過使用import *能夠導入模塊中導出的全部接口:

// myModule.js
export function funcA() {
    console.log('funcA');
}

export function funcB() {
    console.log('funcB');
}
複製代碼
// main.js
import * as m from './myModule';

m.funcA();
m.funcB();
複製代碼

總體加載所在的那個對象(m),應該是能夠靜態分析的,因此不容許運行時改變。因此,下面的寫法都是不容許的:

// main.js
import * as m from './myModule';

// 錯誤
m.name = 'darjun';
m.func = function () {};
複製代碼
Node 中使用 ES6 模塊

Node 因爲已經有 CommonJS 的模塊規範了,與 ES6 模塊不兼容。爲了使用 ES6 模塊,Node 要求 ES6 模塊採用.mjs後綴名,並且文件中只能使用importexport,不能使用require。並且該功能還在試驗階段,Node v8.5.0以上版本,指定--experimental-modules參數才能使用:

// myModule.mjs
var counter = 1;

export function incCounter() {
    console.log('counter:', counter);
    counter++;
}
複製代碼
// main.mjs
import { incCounter } from './myModule';

incCounter();
複製代碼

使用下面命令行運行程序:

$ node --experimental-modules main.mjs
複製代碼

4.總結

隨着 Javascript 在大型項目中佔用舉足輕重的位置,模塊的使用稱爲必然。Node 中使用 CommonJS 規範。ES6 中定義了簡單易用高效的模塊規範。ES6 規範化是個必然的趨勢,因此在掌握當前 CommonJS 規範的前提下,學習 ES6 模塊勢在必行。

5.參考連接

  1. Javascript模塊化編程(一)
  2. Javascript模塊化編程(二)
  3. Javascript模塊化編程(三)
  4. ES6 Module

關於我: 我的主頁 簡書 掘金

相關文章
相關標籤/搜索