JS模組化規範
前言
在ES6之前,JavaScript並未提供一種原生的、語言級別的模組化組織模式,而是將模組化的方法交由開發者來實現。因此出現了很多種JavaScript模組化的實現方式,比如,CommonJS、AMD、CMD等
一、原始模擬模組的一些寫法
在沒有CommonJS
和ES6
的時候,我們想要達到模組化的效果可能有這麼三種:
1.一個函式就是一個模組
<script>
function m1() {}
function m2() {}
</script>
缺點: 汙染了全域性變數,無法保證不會與其它模組發生衝突,而且模組成員之間看不出直接關係。
2.一個物件就是一個模組
物件寫法為了解決上面的缺點,可以把模組寫成一個物件,所有的模組成員都放到這個物件裡面。
<script>
var module1 = new Object({
_sum: 0,
foo1: function() {},
foo2: function() {}
})
</script>
缺點:會暴露所有模組成員,內部的狀態可能被改寫。
3.立即執行函式為一個模組
<script> var module1 = (function() { var _sum = 0; var foo1 = function() {}; var foo2 = function() {}; return { foo1: foo1, foo2: foo2 } })(); </script>
利用立即執行函式內的作用域以及閉包來實現模組功能,匯出我們想要匯出的成員。此時外部程式碼就不能讀取到_sum
了。
二、CommonJS規範
CommonJS是一個有志於構建JavaSctipt生態圈的組織,整個社群致力於提高JavaScript程式的可移植性和可交換性,無論在服務端還是瀏覽器端。
Node.js採用了這個規範。注意在瀏覽器中是不相容CommonJS
的,因為瀏覽器中缺少四個Node.js
環境的變數(module, exports, require, global),只要能夠提供這四個變數,瀏覽器就能載入CommonJS
模組,Browerify
是目前最常用的CommonJS格式轉換工具。
下面從以下四個方面介紹CommonJS:
* 暴露模組
* 引用模組
* 模組識別符號
* CommonJS規範的特點
1.暴露(定義)模組
正確的暴露方式:
暴露模組有兩種方式:
- module.exports = {}
- exports.xxx = 'xxx'
例如有一個m1.js檔案:
第一種暴露方式:
module.exports = {
name: 'aaa',
sex: 'boy'
}
第二種暴露方式:
exports.name = 'aaa';
exports.sex = 'boy'
為什麼可以有這兩種寫法呢?
node為每個模組提供了一個exports變數(可以說是一個物件),指向module.exports。這相當於每個模組中都有一句這樣的命令var exports = module.exports;
這樣,在對外輸出時,可以在這個變數上新增方法或屬性。
但是要注意,不能把exports
直接指向一個值,這樣就相當於切斷了exports和module.exports的關係。
2.引用(引入)模組
對於模組的引用,使用全域性方法require()
就可以了。
注意這個全域性方法是node
中的方法,它不是window下面的。
如果你沒做任何處理直接在html
裡用肯定是不行的。
<body>
<script>
var m1 = require('./m1.js')
console.log(m1)
</script>
</body>
例如上面這樣,你開啟頁面控制檯肯定就報錯了:
Uncaught ReferenceError: require is not defined
at index.html:11
而如果你是在另一個js檔案中引用(例如test.js),並在終端執行node test.js
是可以用的:
test.js:
var m1 = require('./m1.js')
console.log(m1)
那是因為你的電腦上全域性安裝了Node.js
,所以可以這樣玩。
注意:
另外還有一點比較重要,那就是require()
的引數甚至能允許你是一個表示式。也就是說你可以把它設定為一個變數:
test.js:
var m1Url = './m1.js';
var m1 = require(m1Url);
// 甚至做一些字串拼接:
var m1 = require('./m' + '1.js')
但是需要注意,這個傳參可以為表示式並不是require
特有的。因為JS語言是傳值呼叫,函式或者方法在呼叫的時候引數會被先計算出來,因此在我們使用require
方法並傳入表示式的時候,會先計算出表示式的值再傳遞給require
3.模組識別符號
模組識別符號其實就是你在引入模組時呼叫require()
函式的引數。
你會看到我們經常有這樣的用法:
// 直接匯入
const path = require('path');
// 相對路徑
const m1 = require('./m1.js');
// 直接匯入
const lodash = require('lodash');
這其實是因為我們引入的模組會有不同的分類,像path
這種它是Node.js
自帶的模組,m1
是路徑模組,lodash
是我們使用npm i lodash
下載到node_modules
裡的模組。
分為以下三種:
- 核心模組(Node.js自帶的模組)
- 路徑模組(相對或絕對定位開始的模組)
- 自定義模組(node_modules裡的模組)
三種模組的查詢方式:
- 核心模組,直接跳過路徑分析和檔案定位
- 路徑模組,直接得出相對路徑就好了
- 自定義模組,先在當前目錄的
node_modules
裡找這個模組,如果沒有,它會往上一級目錄查詢,查詢上一級的node_modules
,依次往上,直到根目錄都沒有,就丟擲錯誤。
自定義模組的查詢過程:
這個過程其實也叫路徑分析。
現在我們把剛剛的test.js來改一下:
// var m1 = require('./m1.js');
// console.log(m1)
console.log(module.paths)
然後在終端執行:
node test.js
會發現輸出了下面的一個數組:
D:\study\JS\testjs>node test.js
[ 'D:\\study\\JS\\testjs\\node_modules',
'D:\\study\\JS\\node_modules',
'D:\\study\\node_modules',
'D:\\node_modules' ]
這其實就是自定義模組的查詢順序。
檔案定位:
上面已經介紹完路徑分析,但是還有一個問題,就是我們匯入的模組它的字尾(副檔名)是可以省略的啊,那Node
怎麼知道我們是匯入了一個js
還是一個json
呢?這其實就涉及到了檔案定位。
在NodeJS中,省略了副檔名的檔案,會以次補上.js, .node, .json來嘗試,如果傳入的是一個目錄,那麼NodeJS會把它當成一個包來看待,會採用以下方式確定檔名
第一步,找出目錄下的package.json,用JSON.parse()解析出main欄位
第二步,如果main欄位指定的檔案還是省略了擴充套件,那麼會依次補充.js,.node,.json嘗試。
第三步,如果main欄位指定的的檔案不存在,或者根本就不存在package.json,那麼會預設載入這個目錄下的index.js, index.node, index.json檔案。
以上就是檔案定位的過程,再搭配上路徑分析的過程,進行排列組合,這得有多少種可能?所以說,自定義模組的引入是最費效能的。
4.CommonJS規範的特點
- 所有程式碼都執行在模組作用域,不會汙染全域性作用域;
- 模組是同步載入的,即只有載入完成,才能執行後面的操作;
- 模組在首次執行後就會快取,再次載入只返回快取結果,如果想要再次執行,可清除快取;
- CommonJS輸出是值的拷貝(即,
require返回的值是被輸出的值的拷貝,模組內部的變化也不會影響這個值
)。
第一點還是還是好理解的,模組的一個重要功能不就是這個嗎
第二點同步載入,這個寫個案例來驗證一下。
同步載入案例
m1.js:
console.log("我是m1模組")
module.exports = {
name: "m1",
sex: "boy"
}
test.js
var m1 = require('./m1.js');
console.log('我是test模組');
可以看到,test
模組依賴於m1
,且是先下載的m1
模組,所以如果我執行node test.js
,會有以下的執行結果:
我是m1模組
我是test模組
這也就驗證了CommonJS中
,模組是同步載入的,即只有載入完成,才能執行後面的操作。
第三點 模組首次執行後會快取,也可以驗證一下。
模組首次執行後會快取案例:
m1.js:
var name = 'm1';
var sex = 'boy';
exports.name = name;
exports.sex = sex;
test.js
var m1 = require('./m1');
m1.sex = 'girl';
console.log(m1);
var m2 = require('./m1');
console.log(m2)
test
同樣依賴於m1
,但是我會在其中匯入兩次m1
,第一次匯入的時候修改m1.sex
的值,第二次的時候命名為m2
,但是m1
和m2
卻是相等的:
{ name: 'lindaidai', sex: 'girl' }
{ name: 'lindaidai', sex: 'girl' }
也就是說模組在首次執行後就會快取,再次載入只返回快取結果。
那麼就有小夥伴會疑惑了,其實你這樣寫也並不能證明啊,因為你改變了m1.sex
也可能是影響原本m1
模組裡的sex
屬性啊,這樣的話第二次m2
拿到的肯定就是被改變的值了。下面第四個特點就可以很好的解決你這個疑問。
第四點 CommonJS輸出是值的拷貝,也就是說一旦輸出一個值,模組內部的變化就影響不到這個值。
CommonJS輸出是值的拷貝案例:
m1.js:
var name = 'm1';
var sex = 'boy';
var advantage = ['handsome'];
setTimeout(function() {
sex = 'girl';
advantage.push('cute');
}, 500)
exports.name = name;
exports.sex = sex;
exports.advantage = advantage;
test.js:
var m1 = require('./m1');
setTimeout(function() {
console.log('read count after 1000ms in commonjs is', m1.sex)
console.log('read count after 1000ms in commonjs is', m1.advantage)
})
執行node test.js
之後的執行結果是:
read count after 1000ms in commonjs is boy
read count after 1000ms in commonjs is [ 'handsome', 'cute' ]
也就是說,在m1
被引入之後,過了500ms
後我改變了m1
裡的一些屬性,sex
這種基本資料型別是不會被改變的,但是advantage
這種引用型別共用的還是同一個記憶體地址。
如果你這樣寫的話:
m1.js:
var name = 'm1';
var sex = 'boy';
var advantage = ['handsome'];
setTimeout(function() {
sex = 'girl';
// advantage.push('cute');
advantage = ['cute'];
}, 500)
exports.name = name;
exports.sex = sex;
exports.advantage = advantage;
那執行結果肯定就是:
read count after 1000ms in commonjs is boy
read count after 1000ms in commonjs is [ 'handsome' ]
因為相當於m1
的advantage
重新賦值了。
當然,或者如果你的m1.js
中返回的值會有一個函式的話,在test.js
也能拿到變化之後的值了,比如這裡的一個例子:
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter
}
在這裡實際就形成了一個閉包,而counter
屬性就是一個取值器函式。
三、AMD規範
1.產生原因
上面介紹的CommonJS
規範看起來挺好用的,為什麼又還要有其它的規範呢,比如AMD、CMD
,那它們和CommonJS
又有什麼淵源呢
我們知道,模組化這種概念不僅僅適用於伺服器端,客戶端同樣適用。
而CommonJS
規範就不太適合用在客戶端(瀏覽器)環境了,比如上面的那個例子,也就是:
test.js:
const m1 = require(''./m1.js)
console.log(m1)
// 與m1模組無關的一些程式碼
function other() {}
other()
這段程式碼放在瀏覽器中,它會如何執行呢?
- 首先載入
m1.js
- 等m1.js載入完畢之後再執行後面的內容
這點其實在CommonJS規範的特點中已經提到過了。
後面的內容要等待m1
載入完才會執行,如果m1
載入的很慢呢,那不就造成了卡頓,這對於客戶端來說肯定是不友好的,像這種要等待上一個載入完才執行後面內容的情況我們可以叫做“同步載入”,很顯然,這裡我們更希望的是other()
的執行是不需要等m1載入完才執行,也就是我們希望m1
它是“非同步載入”的,這也就是AMD。
在介紹AMD之前讓我們看看CommonJS規範對伺服器端和瀏覽器的不同,它有助於讓你理解為什麼說CommonJS不太適合於客戶端:
- 伺服器端所有的模組都存放在本地硬碟中,可以同步載入完成,等待時間就是硬碟的讀取時間。
- 瀏覽器,所有的模組都放在伺服器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於“假死”狀態。
2.定義並暴露模組
有了上面這層背景,我們就知道了,AMD
它的產生很大一部分原因就是為了能讓我們採用非同步的方式載入模組。
所以現在來讓我們看看它的介紹吧。
AMD
是Asynchronous Module Definition
的縮寫,也就是“非同步模組定義”
。(前面的A
就很好記了,它讓我不自覺的就想到async
這個定義非同步函式的修飾符)
它採用非同步方式載入模組,模組的載入不影響它後面語句的執行。所有依賴這個模組的語句,都定義在一個回撥函式中,等到載入完成之後,這個回撥函式才會執行。
此時就需要另一個重要的方法來定義我們的模組:define()
。
它其實是會有三個引數:
define(id?, dependencies?, factory)
- id: 一個字串,表示模組的名稱,但是是可選的。
- dependencies: 一個數組,是我們當前定義的模組要依賴於哪些模組,陣列中的每一項表示的是要依賴模組的相對路徑,且這個引數也是可選的。
- factory: 工廠方法,一個函式,這裡面就是具體的模組內容了
坑一:
那其實就有一個問題了,看了這麼多的教材,但我想去寫案例的時候,我以為這個define
能直接像require
一樣去用,結果發現控制檯一直在報錯:
ReferenceError: define is not defined
看來它還並不是Node.js
自帶的一個方法啊,搜尋了一下,原來它只是名義上規定了這樣一個方法,但是你真的想要去用還是得使用對應的JavaScript
庫,也就是我們常常聽到的:
目前,主要有兩個JavaScript庫實現了AMD規範:require.js和curl.js
讓我們去requirejs的官網看看如何使用它,
方式1:直接下載require.js檔案
我們新建一個叫AMD
的資料夾,作為AMD
的案例,把require.js檔案放在該目錄下,就可以載入了。
<script src="./require.js"></script>
有人可能會想到,載入這個檔案,也可能會造成網頁失去響應,解決辦法有兩個,一是把它放在底部載入,另一個是寫成下面這樣:
<script src="./require.js" defer async="true"></script>
async屬性表明這個檔案需要非同步載入,避免網頁失去響應。IE不支援這個屬性,只支援defer,所以把defer也寫上。載入require.js以後,下一步就是載入我們自己的程式碼了。
假定我們自己的程式碼檔案為test.js,也放在amd目錄下面,那麼只需簡寫下面這樣就行了:
<script src="./require.js" data-main="./test"></script>
data-main
屬性的作用是,指定網頁程式的主模組,在上例中,就是amd目錄下的test.js檔案,這個檔案會第一個被require.js載入。由於require.js預設的檔案字尾名是js,所以可以把main.js簡寫main。
例項如下:
新建amd資料夾,並下載require.js檔案放在該資料夾下:
新建math.js檔案:
define(['m1'] ,function (m1) {
console.log('我是math,我被載入了。。。')
var add = function(a, b){
return a + b;
}
var print = function() {
console.log(m1.name)
}
return {
add: add,
print: print
}
});
再新建一個test.js檔案並引入math.js模組:
require(['math'], function(math){
console.log('我是test,我被載入了。。。')
console.log(math.add(1, 2))
math.print()
})
function other() {
console.log("我是test模組內的,但是我不依賴math")
}
other()
接下來在test.html檔案引入我們下載的require.js檔案並引入執行test.js檔案
<script src="./require.js"></script>
<script src="./test.js"></script>
我們可以在瀏覽器控制檯看到輸出結果:
我是test模組內的,但是我不依賴math
我是m1,我被載入了。。。
我是math,我被載入了。。。
我是test,我被載入了。。。
3
m1
方式2:使用npm install方式
這個案例我們在Node
環境中測試,在amd
目錄下,執行
npm i requirejs
執行完畢之後,專案的根目錄下出現了依賴包,開啟看了看,確實是下載下來了:
math.js
define(function() {
var add = function(a, b) {
return a + b;
}
return {
add: add
}
})
這個模組很簡單,匯出了一個加法函式。
(至於這裡為什麼add: add要這樣寫,而不是隻簡寫add呢?別忘了這種物件同名屬性簡寫是ES6才出來的)
test.js檔案
var require = require('requirejs') // 注意要再手動引入一下
、
require(['math'], function(math){
console.log('我是test,我被載入了。。。')
console.log(math.add(1, 2))
})
function other() {
console.log("我是test模組內的,但是我不依賴math")
}
other()
在node中執行
D:\study\JS\amd>node test.js
我是test模組內的,但是我不依賴math
我是m1,我被載入了。。。
我是math,我被載入了。。。
我是test,我被載入了。。。
3
3.引用模組
上面已經使用了,也就是require
,基本語法就是:
require([dependencies], function(){});
require()
函式接受兩個引數
- 第一個引數是一個數組,表示所依賴的模組。
- 第二個引數是一個回撥函式,當前面指定的模組都載入成功後,它將呼叫,載入的模組會以引數形式傳入該函式,從而在回撥函式內部就可以使用這些模組。
require()
函式在載入依賴的函式的時候是非同步載入的,這樣瀏覽器不會失去響應,它指定的回撥函式,只有前面的模組都載入成功後,才會執行,解決了依賴性的問題。
四、CMD規範
CMD
(Common module Definition)是seajs
推崇的規範。
CMD
規範是國內發展出來的,就像AMD有個requireJS
,CMD有個瀏覽器實現的SeaJS
,SeaJS
要解決的問題和requireJS
一樣,只不過在模組定義方式和模組載入(可以說執行、解析)實際上有所不同,CMD
則是依賴就近,用的時候再require
。
CMD語法
Sea.js
推崇一個模組一個檔案,遵循統一的寫法,看段程式碼感受一下它是怎麼用的:
define(function(require, exports, module) {
var math = require('./math');
math.print()
})
看著和AMD
有點像,沒錯,其實define()
的引數甚至都是一樣的:
define(id?, dependencies?, factory)
但是區別在於哪裡呢?讓我們看看最後一個factory
引數。
factory函式中是會接收三個引數:
-
require
-
exports
-
module
這三個很好理解,對應著之前的CommonJS那不就是: -
require: 引入某個模組
-
exports: 當前模組的exports,也就是module.exports的簡寫
-
module: 當前這個模組
現在再來說說AMD和CMD的區別。
雖然它們的define()方法的引數都相同,但是:
-
AMD中會把當前模組的依賴模組放到dependencies中載入,並在factory回撥中拿到載入成功的依賴。
-
CMD一般不在dependencies中載入,而是寫在factory中,使用require載入某個依賴模組。
比較有名一點的,seajs
,來看看它推薦的CMD模組書寫格式吧:
// 所有的模組都通過define來定義
define(function(require, exports, module) {
// 通過require引入依賴
var $ = require('jquery');
var spinning = require('./spinning');
// 通過exports對外提供介面
exports.doSomething = ...
// 或者通過module.exports提供整個介面
module.exports = ...
})
五、AMD與CMD的區別
AMD
和CMD
最大的區別是對依賴模組的執行時機處理不同,注意不是載入的時機或方式不同,二者皆為非同步載入模組。
還是上面的那句話,讓我們來看個小例子理解一下。
同樣是math
模組中需要載入m1
模組。
在AMD
中我們會這樣寫:
math.js
define(['m1'], function(m1){
console.log('我是math, 我被載入了')
var add = function(a, b){
return a + b;
}
var print = function() {
console.log(m1.name)
}
return {
add: add,
print: print
}
})
但是對於CMD
,我們會這樣寫:
math.js
define(function(require, exports, module) {
console.log('我是math, 我被載入了。。。')
var m1 = require('m1');
var add = function(a, b) {
return a + b;
}
var print = function() {
console.log(m1.name)
}
module.exports = {
add: add,
print: print
}
})
假如此時m1.js
中有一個語句是在m1
模組被載入的時候打印出“我是m1,我被載入了。。。”
。
執行結果區別:
AMD
,會先載入m1
,我是m1
會先執行CMD
,我是math
會先執行,因為本題中console.log("我是math, 我被載入了。。。")
是放在require('m1')前面的。
現在可以很明顯的看到區別了。
AMD
依賴前置,js
很方便的就知道要載入的是哪個模組了,因為已經在define
的dependencies
引數中就定義好了,會立即載入它。
CMD
是就近依賴,需要把模組變為字串解析一遍才知道依賴了哪些模組。
總結:
1.AMD推崇依賴前置,在定義模組的時候就要宣告其依賴的模組。2.CMD推崇就近依賴,只有在用到某個模組的時候再去require
六、ES6 Module規範
ES6
標準出來後,ES6 Modules
規範算是成為了前端的主流吧,以import
引入模組,export
匯出介面被越來越多的人使用。
下面,我也會從這麼幾個方面來介紹ES6 Modules
規範:
export
命令和import
命令可以出現在模組的任何位置,只要處於模組頂層就可以。如果處於塊級作用域內,就會報錯,這是因為處於條件程式碼塊之中,就沒法做靜態優化了,違背了ES6模組的設計初衷。
1、export匯出模組
export有兩種方式匯出模組:
- 命名式匯出(名稱匯出)
- 預設匯出(自定義匯出)
命名式匯出
一個模組就是一個獨立的檔案,該檔案內部的所有變數,外部無法獲取,如果你希望外部能讀取模組內部的某個變數,就必須使用export關鍵字輸出該變數,下面是一個js檔案,裡面使用export命令輸出變數。
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 2020;
上面程式碼是profile.js檔案,儲存了使用者資訊。ES6將其視為一個模組,裡面export命令對外部輸出三個變數。
export的寫法除了像上面那樣,還有另外一種。
// profile.js
var firstName = 'Michel';
var lastName = 'Jackson';
var year = 2020;
export {firstName, lastName, year};
上面程式碼在export命令後面,使用大括號指定所要輸出的一組變數。它與前一種寫法(直接放置在var語句前)是等價的,但是應該優先考慮使用這種寫法。因為這樣就可以在指令碼尾部,一眼看清楚輸出了哪些變數。
export命令除了輸出變數,還可以輸出函式或類(class)。
// 輸出了一個函式multiply
export function multiply(x, y) {
return x * y;
}
通常情況下,export輸出的變數就是本來的名字,但是可以使用as關鍵字重新命名。
function v1(){}
function v2(){}
export { v1 as a1, v2 as a2 }
需要特別注意的是,export命令規定的是對外的介面,必須與模組內部的變數建立一一對應關係。
// 報錯
export 1;
// 報錯
var m = 1;
export m;
上面兩種寫法都會報錯,因為沒有提供對外的介面。第一種寫法直接輸出1,第二種寫法通過變數m,還是直接輸出1。1只是一個值,不是介面。正確的寫法是下面這樣。
// 寫法一
export var m = 1;
// 寫法二
var m = 1;
export {m};
// 寫法三
var m = 1;
export { n as m };
上面三種寫法都是正確的,規定了對外的介面m。其他指令碼可以通過這個介面,取到值1。它們的實質是,在介面名與模組內部變數之間,建立了一一對應的關係。
同樣的,function和class的輸出,也必須遵守這樣的寫法。
// 報錯
function f() {}
export f;
// 正確
export function f() {};
// 正確
function f() {}
export {f}
預設匯出
在export後面加上一個default就是預設匯出:
//1.
const a = 1;
export default a;
//2.
const a = 1
export default { a };
//3.
export default function() {}; // 可以匯出一個函式
export default class{}; // 也可以是一個類
其實,預設匯出可以理解為另一種形式上的命名匯出,也即是匯出模組的屬性名可以省略不寫(相當於被重寫成了default),當我import的時候可以任意命名。
const a = 1;
export default a;
// 等價於
export { a as defaulf }
2.import匯入模組
import模組匯入與export模組匯出功能相對應,也存在兩種模式匯入方式:命名式匯入(名稱匯入)和預設匯入(定義式匯入)。
來看看寫法:
// 某個模組的匯出module.js
export const a = 1;
// 模組匯入
// 1.這裡的a得和被載入的模組輸出的介面名對應
import { a } from './module'
// 2.使用as重新命名
import { a as myA } from './module'
// 3.若是隻想要執行被載入的模組可以這樣寫,但是即使載入2次也只是執行一次
import './module'
// 4.整體載入
import * as module from './module'
// 5.default介面和具名介面
import module, { a } from './mudule'
第四種寫法會獲取到module中所有匯出的東西,並且賦值到module這個變數下,這樣我們就可以用module.a這種方式來引用a了
3.export...from ...
其實還有一種寫法,可以將export
和from
結合起來用。
例如,我有三個模組a、b、c
c
模組現在想要引入a
模組,但是它不直接引用a
,而是通過b
模組來引用,那麼你可能會想到b
引用a
再把a
輸出:
//b.js
import { someVarible } from './a';
export { someVariable };
這還只是一個變數,我們得匯入再匯出,若是有很多個變數需要這樣,那無疑會增加很多程式碼量。
所以這時候可以用下面這種方式實現:
export { someVariable } from './a';
不過這種方式有一點需要注意:
這樣的方式不會將資料新增到該聚合模組的作用域,也就是說,你無法在該模組(也就是b)中使用someVariable
。
4.ES6 Modules規範的特點
總結一下它的特點:
- 輸出使用export
- 輸入使用import
- 可以使用export...from...這種寫法來達到一個
中轉
的效果 - 輸入的模組變數是不可重新賦值的,它只是個可讀引用,不過卻可以改寫屬性
- export命令和import命令可以出現在模組的任何位置,只要處於模組頂層就可以。如果處於塊級作用域內,就會報錯,這是因為處於條件程式碼之中,就沒法做靜態優化了,違背了ES6模組的設計初衷。
- import命令具有提升效果,會提升到整個模組的頭部,首先執行。
5.Bable下的ES6模組轉換
還有一點就是,如果你有使用過一些ES6的Babel的話,你會發現當使用export/import
的時候,Babel也會把它轉換為exports/require
的形式。
例如我的輸出:
m1.js:
export const count = 0;
我的輸入:
index.js:
import { count } from './m1.js'
console.log(count)
當使用Babel編譯之後,各自會被轉換為:
m1.js:
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.count = void 0;
const count = 0;
exports.count = count;
index.js:
"use strict";
var _m = require("./m1.js");
console.log(_m.count);
正是因為這種轉換關係,才能讓我們把export
和import
結合起來用:
也就是說你可以這樣用:
// 輸出模組m1.js
exports.count = 0;
// index.js中引入
import {count} from './m1.js'
console.log(count)
七、CommonJS與ES6 Modules規範的區別
- CommonJS模組是執行時載入,ES6 Modules是編譯時輸出介面
- CommonJS輸出是值的拷貝,ES6 Modules輸出的是值的引用,被輸出模組的內部的改變會影響引用的改變
- CommonJS匯入的模組路徑可以是一個表示式,因為它使用的是
require()
方法,而ES6 Modules只能是字串 - CommonJS
this
指向當前模組,ES6 Modules的this
指向undefined
- 且ES6 Modules中沒有這些頂層變數:
arguments
、require
、module
、exports
、__filename
、__dirname
關於第一個差異,是因為CommonJS載入的是一個物件(即module.exports
屬性),該物件只有在指令碼執行完才會生成。而ES6模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。
參考:
https://javascript.ruanyifeng.com/nodejs/module.html
https://juejin.cn/post/6844904145443356680
https://www.cnblogs.com/dolphinX/p/4381855.html