本文首發(fā)于 vivo互聯(lián)網(wǎng)技術(shù) 微信公眾號(hào)
鏈接: https://mp.weixin.qq.com/s/15sedEuUVTsgyUm1lswrKA
作者:Morrain

一、前言
上一篇《前端科普系列(2):Node.js 換個(gè)角度看世界》,我們聊了 Node.js 相關(guān)的東西,Node.js 能在誕生后火到如此一塌糊涂,離不開(kāi)它成熟的模塊化實(shí)現(xiàn),Node.js 的模塊化是在 CommonJS 規(guī)范的基礎(chǔ)上實(shí)現(xiàn)的。那 CommonJS 又是什么呢?
先來(lái)看下,它在維基百科上的定義:
CommonJS 是一個(gè)項(xiàng)目,其目標(biāo)是為 JavaScript 在網(wǎng)頁(yè)瀏覽器之外創(chuàng)建模塊約定。創(chuàng)建這個(gè)項(xiàng)目的主要原因是當(dāng)時(shí)缺乏普遍可接受形式的 JavaScript 腳本模塊單元,模塊在與運(yùn)行JavaScript 腳本的常規(guī)網(wǎng)頁(yè)瀏覽器所提供的不同的環(huán)境下可以重復(fù)使用。
我們知道,很長(zhǎng)一段時(shí)間 JavaScript 語(yǔ)言是沒(méi)有模塊化的概念的,直到 Node.js 的誕生,把 JavaScript 語(yǔ)言帶到服務(wù)端后,面對(duì)文件系統(tǒng)、網(wǎng)絡(luò)、操作系統(tǒng)等等復(fù)雜的業(yè)務(wù)場(chǎng)景,模塊化就變得不可或缺。于是 Node.js 和 CommonJS 規(guī)范就相得益彰、相映成輝,共同走入開(kāi)發(fā)者的視線。

由此可見(jiàn),CommonJS 最初是服務(wù)于服務(wù)端的,所以我說(shuō) CommonJS 不是前端,但它的載體是前端語(yǔ)言 JavaScript,為后面前端模塊化的盛行產(chǎn)生了深遠(yuǎn)的影響,奠定了結(jié)實(shí)的基礎(chǔ)。CommonJS:不是前端卻革命了前端!
二、為什么需要模塊化
1、沒(méi)有模塊化時(shí),前端是什么樣子
在之前的《Web:一路前行一路忘川》中,我們提到過(guò) JavaScript 誕生之初只是作為一個(gè)腳本語(yǔ)言來(lái)使用,做一些簡(jiǎn)單的表單校驗(yàn)等等。所以代碼量很少,最開(kāi)始都是直接寫到 <script> 標(biāo)簽里,如下所示:
// index.html
<script>
var name = 'morrain'
var age = 18
</script>
隨著業(yè)務(wù)進(jìn)一步復(fù)雜,Ajax 誕生以后,前端能做的事情越來(lái)越多,代碼量飛速增長(zhǎng),開(kāi)發(fā)者們開(kāi)始把 JavaScript 寫到獨(dú)立的 js 文件中,與 html 文件解耦。像下面這樣:
// index.html
<script src="./mine.js"></script>
// mine.js
var name = 'morrain'
var age = 18
再后來(lái),更多的開(kāi)發(fā)者參與進(jìn)來(lái),更多的 js 文件被引入進(jìn)來(lái):
// index.html
<script src="./mine.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script>
// mine.js
var name = 'morrain'
var age = 18
// a.js
var name = 'lilei'
var age = 15
// b.js
var name = 'hanmeimei'
var age = 13
不難發(fā)現(xiàn),問(wèn)題已經(jīng)來(lái)了!JavaScript 在 ES6 之前是沒(méi)有模塊系統(tǒng),也沒(méi)有封閉作用域的概念的,所以上面三個(gè) js 文件里申明的變量都會(huì)存在于全局作用域中。不同的開(kāi)發(fā)者維護(hù)不同的 js 文件,很難保證不和其它 js 文件沖突。全局變量污染開(kāi)始成為開(kāi)發(fā)者的噩夢(mèng)。
2、模塊化的原型
為了解決全局變量污染的問(wèn)題,開(kāi)發(fā)者開(kāi)始使用命名空間的方法,既然命名會(huì)沖突,那就加上命名空間唄,如下所示:
// index.html
<script src="./mine.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script>
// mine.js
app.mine = {}
app.mine.name = 'morrain'
app.mine.age = 18
// a.js
app.moduleA = {}
app.moduleA.name = 'lilei'
app.moduleA.age = 15
// b.js
app.moduleB = {}
app.moduleB.name = 'hanmeimei'
app.moduleB.age = 13
此時(shí),已經(jīng)開(kāi)始有隱隱約約的模塊化的概念,只不過(guò)是用命名空間實(shí)現(xiàn)的。這樣在一定程度上是解決了命名沖突的問(wèn)題, b.js 模塊的開(kāi)發(fā)者,可以很方便的通過(guò) app.moduleA.name 來(lái)取到模塊A中的名字,但是也可以通過(guò) app.moduleA.name = 'rename' 來(lái)任意改掉模塊A中的名字,而這件事情,模塊A卻毫不知情!這顯然是不被允許的。
聰明的開(kāi)發(fā)者又開(kāi)始利用 JavaScript 語(yǔ)言的函數(shù)作用域,使用閉包的特性來(lái)解決上面的這一問(wèn)題。
// index.html
<script src="./mine.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script>
// mine.js
app.mine = (function(){
var name = 'morrain'
var age = 18
return {
getName: function(){
return name
}
}
})()
// a.js
app.moduleA = (function(){
var name = 'lilei'
var age = 15
return {
getName: function(){
return name
}
}
})()
// b.js
app.moduleB = (function(){
var name = 'hanmeimei'
var age = 13
return {
getName: function(){
return name
}
}
})()
現(xiàn)在 b.js 模塊可以通過(guò)
app.moduleA.getName() 來(lái)取到模塊A的名字,但是各個(gè)模塊的名字都保存在各自的函數(shù)內(nèi)部,沒(méi)有辦法被其它模塊更改。這樣的設(shè)計(jì),已經(jīng)有了模塊化的影子,每個(gè)模塊內(nèi)部維護(hù)私有的東西,開(kāi)放接口給其它模塊使用,但依然不夠優(yōu)雅,不夠完美。譬如上例中,模塊B可以取到模塊A的東西,但模塊A卻取不到模塊B的,因?yàn)樯厦孢@三個(gè)模塊加載有先后順序,互相依賴。當(dāng)一個(gè)前端應(yīng)用業(yè)務(wù)規(guī)模足夠大后,這種依賴關(guān)系又變得異常難以維護(hù)。
綜上所述,前端需要模塊化,并且模塊化不光要處理全局變量污染、數(shù)據(jù)保護(hù)的問(wèn)題,還要很好的解決模塊之間依賴關(guān)系的維護(hù)。
三、CommonJS 規(guī)范簡(jiǎn)介
既然 JavaScript 需要模塊化來(lái)解決上面的問(wèn)題,那就需要制定模塊化的規(guī)范,CommonJS 就是解決上面問(wèn)題的模塊化規(guī)范,規(guī)范就是規(guī)范,沒(méi)有為什么,就和編程語(yǔ)言的語(yǔ)法一樣。我們一起來(lái)看看。
1、CommonJS 概述
Node.js 應(yīng)用由模塊組成,每個(gè)文件就是一個(gè)模塊,有自己的作用域。在一個(gè)文件里面定義的變量、函數(shù)、類,都是私有的,對(duì)其他文件不可見(jiàn)。
// a.js
var name = 'morrain'
var age = 18
上面代碼中,a.js 是 Node.js 應(yīng)用中的一個(gè)模塊,里面申明的變量 name 和 age 是 a.js 私有的,其它文件都訪問(wèn)不到。
CommonJS 規(guī)范還規(guī)定,每個(gè)模塊內(nèi)部有兩個(gè)變量可以使用,require 和 module。
require 用來(lái)加載某個(gè)模塊
module 代表當(dāng)前模塊,是一個(gè)對(duì)象,保存了當(dāng)前模塊的信息。exports 是 module 上的一個(gè)屬性,保存了當(dāng)前模塊要導(dǎo)出的接口或者變量,使用 require 加載的某個(gè)模塊獲取到的值就是那個(gè)模塊使用 exports 導(dǎo)出的值
// a.js
var name = 'morrain'
var age = 18
module.exports.name = name
module.exports.getAge = function(){
return age
}
//b.js
var a = require('a.js')
console.log(a.name) // 'morrain'
console.log(a.getAge())// 18
2、CommonJS 之 exports
為了方便,Node.js 在實(shí)現(xiàn) CommonJS 規(guī)范時(shí),為每個(gè)模塊提供一個(gè) exports的私有變量,指向 module.exports。你可以理解為 Node.js 在每個(gè)模塊開(kāi)始的地方,添加了如下這行代碼。
var exports = module.exports
于是上面的代碼也可以這樣寫:
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function(){
return age
}

有一點(diǎn)要尤其注意,exports 是模塊內(nèi)的私有局部變量,它只是指向了 module.exports,所以直接對(duì) exports 賦值是無(wú)效的,這樣只是讓 exports 不再指向module.exports了而已。
如下所示:
// a.js
var name = 'morrain'
var age = 18
exports = name

如果一個(gè)模塊的對(duì)外接口,就是一個(gè)單一的值,可以使用 module.exports 導(dǎo)出
// a.js
var name = 'morrain'
var age = 18
module.exports = name
3、CommonJS 之 require
require 命令的基本功能是,讀入并執(zhí)行一個(gè) js 文件,然后返回該模塊的 exports 對(duì)象。如果沒(méi)有發(fā)現(xiàn)指定模塊,會(huì)報(bào)錯(cuò)。
第一次加載某個(gè)模塊時(shí),Node.js 會(huì)緩存該模塊。以后再加載該模塊,就直接從緩存取出該模塊的 module.exports 屬性返回了。
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function(){
return age
}
// b.js
var a = require('a.js')
console.log(a.name) // 'morrain'
a.name = 'rename'
var b = require('a.js')
console.log(b.name) // 'rename'
如上所示,第二次 require 模塊A時(shí),并沒(méi)有重新加載并執(zhí)行模塊A。而是直接返回了第一次 require 時(shí)的結(jié)果,也就是模塊A的 module.exports。

還一點(diǎn)需要注意,CommonJS 模塊的加載機(jī)制是,require 的是被導(dǎo)出的值的拷貝。也就是說(shuō),一旦導(dǎo)出一個(gè)值,模塊內(nèi)部的變化就影響不到這個(gè)值 。
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.age = age
exports.setAge = function(a){
age = a
}
// b.js
var a = require('a.js')
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 18
四、CommonJS 實(shí)現(xiàn)
了解 CommonJS 的規(guī)范后,不難發(fā)現(xiàn)我們?cè)趯懛?CommonJS 規(guī)范的模塊時(shí),無(wú)外乎就是使用了 require 、 exports 、 module 三個(gè)東西,然后一個(gè) js 文件就是一個(gè)模塊。如下所示:
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = function () {
return age
}
// b.js
var a = require('a.js')
console.log('a.name=', a.name)
console.log('a.age=', a.getAge())
var name = 'lilei'
var age = 15
exports.name = name
exports.getAge = function () {
return age
}
// index.js
var b = require('b.js')
console.log('b.name=',b.name)
如果我們向一個(gè)立即執(zhí)行函數(shù)提供 require 、 exports 、 module 三個(gè)參數(shù),模塊代碼放在這個(gè)立即執(zhí)行函數(shù)里面。模塊的導(dǎo)出值放在 module.exports 中,這樣就實(shí)現(xiàn)了模塊的加載。如下所示:
(function(module, exports, require) {
// b.js
var a = require("a.js")
console.log('a.name=', a.name)
console.log('a.age=', a.getAge())
var name = 'lilei'
var age = 15
exports.name = name
exports.getAge = function () {
return age
}
})(module, module.exports, require)
知道這個(gè)原理后,就很容易把符合 CommonJS 模塊規(guī)范的項(xiàng)目代碼,轉(zhuǎn)化為瀏覽器支持的代碼。很多工具都是這么實(shí)現(xiàn)的,從入口模塊開(kāi)始,把所有依賴的模塊都放到各自的函數(shù)中,把所有模塊打包成一個(gè)能在瀏覽器中運(yùn)行的 js 文件。譬如 Browserify 、webpack 等等。
我們以 webpack 為例,看看如何實(shí)現(xiàn)對(duì) CommonJS 規(guī)范的支持。我們使用 webpack 構(gòu)建時(shí),把各個(gè)模塊的文件內(nèi)容按照如下格式打包到一個(gè) js 文件中,因?yàn)樗且粋€(gè)立即執(zhí)行的匿名函數(shù),所以可以在瀏覽器直接運(yùn)行。
// bundle.js
(function (modules) {
// 模塊管理的實(shí)現(xiàn)
})({
'a.js': function (module, exports, require) {
// a.js 文件內(nèi)容
},
'b.js': function (module, exports, require) {
// b.js 文件內(nèi)容
},
'index.js': function (module, exports, require) {
// index.js 文件內(nèi)容
}
})
接下來(lái),我們需要按照 CommonJS 的規(guī)范,去實(shí)現(xiàn)模塊管理的內(nèi)容。首先我們知道,CommonJS 規(guī)范有說(shuō)明,加載過(guò)的模塊會(huì)被緩存,所以需要一個(gè)對(duì)象來(lái)緩存已經(jīng)加載過(guò)的模塊,然后需要一個(gè) require 函數(shù)來(lái)加載模塊,在加載時(shí)要生成一個(gè) module,并且 module 上 要有一個(gè) exports 屬性,用來(lái)接收模塊導(dǎo)出的內(nèi)容。
// bundle.js
(function (modules) {
// 模塊管理的實(shí)現(xiàn)
var installedModules = {}
/**
* 加載模塊的業(yè)務(wù)邏輯實(shí)現(xiàn)
* @param {String} moduleName 要加載的模塊名
*/
var require = function (moduleName) {
// 如果已經(jīng)加載過(guò),就直接返回
if (installedModules[moduleName]) return installedModules[moduleName].exports
// 如果沒(méi)有加載,就生成一個(gè) module,并放到 installedModules
var module = installedModules[moduleName] = {
moduleName: moduleName,
exports: {}
}
// 執(zhí)行要加載的模塊
modules[moduleName].call(modules.exports, module, module.exports, require)
return module.exports
}
return require('index.js')
})({
'a.js': function (module, exports, require) {
// a.js 文件內(nèi)容
},
'b.js': function (module, exports, require) {
// b.js 文件內(nèi)容
},
'index.js': function (module, exports, require) {
// index.js 文件內(nèi)容
}
})
可以看到, CommonJS 核心的規(guī)范,上面的實(shí)現(xiàn)中都滿足了。非常簡(jiǎn)單,沒(méi)想像的那么難。
五、其它前端模塊化的方案
我們對(duì) CommonJS 的規(guī)范已經(jīng)非常熟悉了,require 命令的基本功能是,讀入并執(zhí)行一個(gè) js 文件,然后返回該模塊的 exports 對(duì)象,這在服務(wù)端是可行的,因?yàn)榉?wù)端加載并執(zhí)行一個(gè)文件的時(shí)間消費(fèi)是可以忽略的,模塊的加載是運(yùn)行時(shí)同步加載的,require 命令執(zhí)行完后,文件就執(zhí)行完了,并且成功拿到了模塊導(dǎo)出的值。
這種規(guī)范天生就不適用于瀏覽器,因?yàn)樗峭降???上攵?,瀏覽器端每加載一個(gè)文件,要發(fā)網(wǎng)絡(luò)請(qǐng)求去取,如果網(wǎng)速慢,就非常耗時(shí),瀏覽器就要一直等 require 返回,就會(huì)一直卡在那里,阻塞后面代碼的執(zhí)行,從而阻塞頁(yè)面渲染,使得頁(yè)面出現(xiàn)假死狀態(tài)。
為了解決這個(gè)問(wèn)題,后面發(fā)展起來(lái)了眾多的前端模塊化規(guī)范,包括 CommonJS 大致有如下幾種:

1、AMD (Asynchronous Module Definition)
在聊 AMD 之前,先熟悉一下 RequireJS。
官網(wǎng)是這么介紹它的:
"RequireJS is a JavaScript file and module loader. It is optimized for in-browser use, but it can be used in other JavaScript environments, like Rhino and Node. Using a modular script loader like RequireJS will improve the speed and quality of your code."
翻譯過(guò)來(lái)大致就是:
RequireJS 是一個(gè) js 文件和模塊加載器。它非常適合在瀏覽器中使用,但它也可以用在其他 js 環(huán)境, 就像 Rhino 和 Node。使用 RequireJS 加載模塊化腳本能提高代碼的加載速度和質(zhì)量。
它解決了 CommonJS 規(guī)范不能用于瀏覽器端的問(wèn)題,而 AMD 就是 RequireJS 在推廣過(guò)程中對(duì)模塊定義的規(guī)范化產(chǎn)出。
來(lái)看看 AMD 規(guī)范的實(shí)現(xiàn):
<script src="require.js"></script>
<script src="a.js"></script>
首先要在 html 文件中引入 require.js 工具庫(kù),就是這個(gè)庫(kù)提供了定義模塊、加載模塊等功能。它提供了一個(gè)全局的 define 函數(shù)用來(lái)定義模塊。所以在引入 require.js 文件后,再引入的其它文件,都可以使用 define 來(lái)定義模塊。
define(id?, dependencies?, factory)
id:可選參數(shù),用來(lái)定義模塊的標(biāo)識(shí),如果沒(méi)有提供該參數(shù),就使用 js 文件名(去掉拓展名)對(duì)于一個(gè) js 文件只定義了一個(gè)模塊時(shí),這個(gè)參數(shù)是可以省略的。dependencies:可選參數(shù),是一個(gè)數(shù)組,表示當(dāng)前模塊的依賴,如果沒(méi)有依賴可以不傳 factory:工廠方法,模塊初始化要執(zhí)行的函數(shù)或?qū)ο?。如果為函?shù),它應(yīng)該只被執(zhí)行一次,返回值便是模塊要導(dǎo)出的值。如果是對(duì)象,此對(duì)象應(yīng)該為模塊的輸出值。
所以模塊A可以這么定義:
// a.js
define(function(){
var name = 'morrain'
var age = 18
return {
name,
getAge: () => age
}
})
// b.js
define(['a.js'], function(a){
var name = 'lilei'
var age = 15
console.log(a.name) // 'morrain'
console.log(a.getAge()) // 18
return {
name,
getAge: () => age
}
})
它采用異步方式加載模塊,模塊的加載不影響它后面語(yǔ)句的運(yùn)行。所有依賴這個(gè)模塊的語(yǔ)句,都定義在回調(diào)函數(shù)中,等到加載完成之后,這個(gè)回調(diào)函數(shù)才會(huì)運(yùn)行。
RequireJS 的基本思想是,通過(guò) define 方法,將代碼定義為模塊。當(dāng)這個(gè)模塊被 require 時(shí),它開(kāi)始加載它依賴的模塊,當(dāng)所有依賴的模塊加載完成后,開(kāi)始執(zhí)行回調(diào)函數(shù),返回值是該模塊導(dǎo)出的值。AMD 是 "Asynchronous Module Definition" 的縮寫,意思就是"異步模塊定義"。
2、CMD (Common Module Definition)
和 AMD 類似,CMD 是 Sea.js 在推廣過(guò)程中對(duì)模塊定義的規(guī)范化產(chǎn)出。Sea.js 是阿里的玉伯寫的。它的誕生在 RequireJS 之后,玉伯覺(jué)得 AMD 規(guī)范是異步的,模塊的組織形式不夠自然和直觀。于是他在追求能像 CommonJS 那樣的書寫形式。于是就有了 CMD 。
Sea.js 官網(wǎng)這么介紹 Sea.js:
"Sea.js 追求簡(jiǎn)單、自然的代碼書寫和組織方式,具有以下核心特性:"
"簡(jiǎn)單友好的模塊定義規(guī)范:Sea.js 遵循 CMD 規(guī)范,可以像 Node.js 一般書寫模塊代碼。自然直觀的代碼組織方式:依賴的自動(dòng)加載、配置的簡(jiǎn)潔清晰,可以讓我們更多地享受編碼的樂(lè)趣。"
來(lái)看看 CMD 規(guī)范的實(shí)現(xiàn):
<script src="sea.js"></script>
<script src="a.js"></script>
首先要在 html 文件中引入 sea.js 工具庫(kù),就是這個(gè)庫(kù)提供了定義模塊、加載模塊等功能。它提供了一個(gè)全局的 define 函數(shù)用來(lái)定義模塊。所以在引入 sea.js 文件后,再引入的其它文件,都可以使用 define 來(lái)定義模塊。
// 所有模塊都通過(guò) define 來(lái)定義
define(function(require, exports, module) {
// 通過(guò) require 引入依賴
var a = require('xxx')
var b = require('yyy')
// 通過(guò) exports 對(duì)外提供接口
exports.doSomething = ...
// 或者通過(guò) module.exports 提供整個(gè)接口
module.exports = ...
})
// a.js
define(function(require, exports, module){
var name = 'morrain'
var age = 18
exports.name = name
exports.getAge = () => age
})
// b.js
define(function(require, exports, module){
var name = 'lilei'
var age = 15
var a = require('a.js')
console.log(a.name) // 'morrain'
console.log(a.getAge()) //18
exports.name = name
exports.getAge = () => age
})
Sea.js 可以像 CommonsJS 那樣同步的形式書寫模塊代碼的秘訣在于:當(dāng) b.js 模塊被 require 時(shí),b.js 加載后,Sea.js 會(huì)掃描 b.js 的代碼,找到 require 這個(gè)關(guān)鍵字,提取所有的依賴項(xiàng),然后加載,等到依賴的所有模塊加載完成后,執(zhí)行回調(diào)函數(shù),此時(shí)再執(zhí)行到 require('a.js') 這行代碼時(shí),a.js 已經(jīng)加載好在內(nèi)存中了
3、ES6 Module
前面提到的 CommonJS 是服務(wù)于服務(wù)端的,而 AMD、CMD 是服務(wù)于瀏覽器端的,但它們都有一個(gè)共同點(diǎn):都在代碼運(yùn)行后才能確定導(dǎo)出的內(nèi)容,CommonJS 實(shí)現(xiàn)中可以看到。
還有一點(diǎn)需要注意,AMD 和 CMD 是社區(qū)的開(kāi)發(fā)者們制定的模塊加載方案,并不是語(yǔ)言層面的標(biāo)準(zhǔn)。從 ES6 開(kāi)始,在語(yǔ)言標(biāo)準(zhǔn)的層面上,實(shí)現(xiàn)了模塊化功能,而且實(shí)現(xiàn)得相當(dāng)簡(jiǎn)單,完全可以取代 CommonJS 和 CMD、AMD 規(guī)范,成為瀏覽器和服務(wù)器通用的模塊解決方案。
事實(shí)也是如些,早在2013年5月,Node.js 的包管理器 NPM 的作者 Isaac Z. Schlueter 說(shuō)過(guò) CommonJS 已經(jīng)過(guò)時(shí),Node.js 的內(nèi)核開(kāi)發(fā)者已經(jīng)決定廢棄該規(guī)范。原因主要有兩個(gè),一個(gè)是因?yàn)?Node.js 本身也不是完全采用 CommonJS 的規(guī)范,譬如在CommonJS 之 exports 中的提到 exports 屬性就是 Node.js 自己加的,Node.js 當(dāng)時(shí)是決定不再跟隨 CommonJS 的發(fā)展而發(fā)展了。二來(lái)就是 Node.js 也在逐步用 ES6 Module 替代 CommonJS。
2017.9.12 Node.js 發(fā)布的 8.5.0 版本開(kāi)始支持 ES6 Module。只不過(guò)是處于實(shí)驗(yàn)階段。需要添加 --experimental-modules 參數(shù)。

2019.11.21 Node.js 發(fā)布的 13.2.0 版本中取消了 --experimental-modules 參數(shù) ,也就是說(shuō)從 v13.2 版本開(kāi)始,Node.js 已經(jīng)默認(rèn)打開(kāi)了 ES6 Module 的支持。

(1)****ES6 Module 語(yǔ)法
任何模塊化,都必須考慮的兩個(gè)問(wèn)題就是導(dǎo)入依賴和導(dǎo)出接口。ES6 Module 也是如此,模塊功能主要由兩個(gè)命令構(gòu)成:export 和 import。export 命令用于導(dǎo)出模塊的對(duì)外接口,import 命令用于導(dǎo)入其他模塊導(dǎo)出的內(nèi)容。
具體語(yǔ)法講解請(qǐng)參考阮一峰老師的教程,示例如下:
// a.js
export const name = 'morrain'
const age = 18
export function getAge () {
return age
}
//等價(jià)于
const name = 'morrain'
const age = 18
function getAge (){
return age
}
export {
name,
getAge
}
使用 export 命令定義了模塊的對(duì)外接口以后,其他 JavaScript 文件就可以通過(guò) import 命令加載這個(gè)模塊。
// b.js
import { name as aName, getAge } from 'a.js'
export const name = 'lilei'
console.log(aName) // 'morrain'
const age = getAge()
console.log(age) // 18
// 等價(jià)于
import * as a from 'a.js'
export const name = 'lilei'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18
除了指定加載某個(gè)輸出值,還可以使用整體加載,即用星號(hào)(*)指定一個(gè)對(duì)象,所有輸出值都加載在這個(gè)對(duì)象上面。
從上面的例子可以看到,使用 import 命令的時(shí)候,用戶需要知道所要導(dǎo)入的變量名,這有時(shí)候比較麻煩,于是 ES6 Module 規(guī)定了一種方便的用法,使用 export default命令,為模塊指定默認(rèn)輸出。
// a.js
const name = 'morrain'
const age = 18
function getAge () {
return age
}
export default {
name,
getAge
}
// b.js
import a from 'a.js'
console.log(a.name) // 'morrin'
const age = a.getAge()
console.log(age) // 18
顯然,一個(gè)模塊只能有一個(gè)默認(rèn)輸出,因此 export default 命令只能使用一次。同時(shí)可以看到,這時(shí) import 命令后面,不需要再使用大括號(hào)了。
除了基礎(chǔ)的語(yǔ)法外,還有 as 的用法、export 和 import 復(fù)合寫法、export * from 'a'、import()動(dòng)態(tài)加載 等內(nèi)容,可以自行學(xué)習(xí)。
前面提到的 Node.js 已經(jīng)默認(rèn)支持 ES6 Module ,瀏覽器也已經(jīng)全面支持 ES6 Module。至于 Node.js 和 瀏覽器 如何使用 ES6 Module,可以自行學(xué)習(xí)。
(2)ES6 Module 和 CommonJS 的區(qū)別
CommonJS 只能在運(yùn)行時(shí)確定導(dǎo)出的接口,實(shí)際導(dǎo)出的就是一個(gè)對(duì)象。而 ES6 Module 的設(shè)計(jì)思想是盡量的靜態(tài)化,使得編譯時(shí)就能確定模塊的依賴關(guān)系,以及導(dǎo)入和導(dǎo)出的變量,也就是所謂的"編譯時(shí)加載"。
正因?yàn)槿绱?,import 命令具有提升效果,會(huì)提升到整個(gè)模塊的頭部,首先執(zhí)行。下面的代碼是合法的,因?yàn)?import 的執(zhí)行早于 getAge 的調(diào)用。
// a.js
export const name = 'morrain'
const age = 18
export function getAge () {
return age
}
// b.js
const age = getAge()
console.log(age) // 18
import { getAge } from 'a.js'
也正因?yàn)?ES6 Module 是編譯時(shí)加載, 所以不能使用表達(dá)式和變量,因?yàn)檫@些是只有在運(yùn)行時(shí)才能得到結(jié)果的語(yǔ)法結(jié)構(gòu)。如下所示:
// 報(bào)錯(cuò)
import { 'n' + 'ame' } from 'a.js'
// 報(bào)錯(cuò)
let module = 'a.js'
import { name } from module
前面在CommonJS 之 require有提到,require 的是被導(dǎo)出的值的拷貝。也就是說(shuō),一旦導(dǎo)出一個(gè)值,模塊內(nèi)部的變化就影響不到這個(gè)值。一起來(lái)看看,ES Module是什么樣的。
先回顧一下之前的例子:
// a.js
var name = 'morrain'
var age = 18
exports.name = name
exports.age = age
exports.setAge = function(a){
age = a
}
// b.js
var a = require('a.js')
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 18
使用 ES6 Module 來(lái)實(shí)現(xiàn)這個(gè)例子:
// a.js
var name = 'morrain'
var age = 18
const setAge = a => age = a
export {
name,
age,
setAge
}
// b.js
import * as a from 'a.js'
console.log(a.age) // 18
a.setAge(19)
console.log(a.age) // 19
ES6 Module 是 ES6 中對(duì)模塊的規(guī)范,ES6 是 ECMAScript 6.0 的簡(jiǎn)稱,是 JavaScript 語(yǔ)言的下一代標(biāo)準(zhǔn),已經(jīng)在 2015 年 6 月正式發(fā)布了。我們?cè)诘谝还?jié)的《Web:一路前行一路忘川》中提過(guò),ES6 從制定到發(fā)布?xì)v經(jīng)了十幾年,引入了很多的新特性以及新的機(jī)制,對(duì)于開(kāi)發(fā)者而言,學(xué)習(xí)成本還是蠻大的。
下一篇,聊聊 ES6+ 和 Babel,敬請(qǐng)期待……

六、參考文獻(xiàn)