模塊(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
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) {
// 模塊文件內容
}
複製代碼
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
};
複製代碼
exports
和module.exports
開始指向的是同一個對象。寫法一經過exports
設置屬性,一樣對module.exports
也可見。寫法二經過module.exports
設置屬性也能夠導出。 寫法三直接設置module.exports
就更不用說了。
建議在程序開發中,堅持一種寫法。我的以爲寫法三顯示設置相對較容易理解。
**有一點須要注意:不是隻有對象能夠導出,函數、類等值也能夠。**例以下面就導出了一個函數:
function addTwoNumber(a, b) {
return a + b;
}
module.exports = addTwoNumber;
複製代碼
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 模塊最重要的特性是「靜態加載」,導入的接口是隻讀的,不能修改。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
,以別名func2
和myFunc
導出函數funcB
。func2
和myFunc
都是指向同一個函數funcB
的。下面看看使用這個模塊:
// main.js
import { func1, func2, myFunc } from './myModule';
複製代碼
一樣的,導入模塊時也能夠指定別名:
// main.js
import { func1 as func } from './myModule';
複製代碼
上面介紹的模塊導入必須知道接口名字。有時候,用戶學習一個模塊時但願可以快速上手,不想去看文檔(怎麼會有這個懶的人🤣)。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 因爲已經有 CommonJS 的模塊規範了,與 ES6 模塊不兼容。爲了使用 ES6 模塊,Node 要求 ES6 模塊採用.mjs
後綴名,並且文件中只能使用import
和export
,不能使用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
複製代碼
隨着 Javascript 在大型項目中佔用舉足輕重的位置,模塊的使用稱爲必然。Node 中使用 CommonJS 規範。ES6 中定義了簡單易用高效的模塊規範。ES6 規範化是個必然的趨勢,因此在掌握當前 CommonJS 規範的前提下,學習 ES6 模塊勢在必行。