前言
The last time, I have learnedjavascript
【THE LAST TIME】一直是我想寫的一個系列,旨在厚積薄發,重溫前端。css
也是對本身的查缺補漏和技術分享。html
歡迎你們多多評論指點吐槽。前端
系列文章均首發於公衆號【全棧前端精選】,筆者文章集合詳見GitHub 地址:Nealyang/personalBlog。目錄和發文順序皆爲暫定java
隨着互聯網的發展,前端開發也變的愈來愈複雜,從一開始的表單驗證到如今動不動上千上萬行代碼的項目開發,團隊協做就是咱們不可避免的工做方式,爲了更好地管理功能邏輯,模塊化的概念也就漸漸產生了。node
好的書籍📚會分章節,好的代碼得分模塊。jquery
JavaScript 在早期的設計中就沒有模塊、包甚至類的概念,雖然 ES6
中有了 class
關鍵字,那也只是個語法糖。隨意隨着項目複雜度的增長,開發者必然須要模擬類的功能,來隔離、封裝、組織複雜的 JavaScript 代碼,而這種封裝和隔離,也被被咱們稱之爲模塊化。git
模塊就是一個實現特定功能的文件 or 代碼塊。隨着前端工程體系建設的愈發成熟,或許模塊化的概念已經在前端圈子裏已經耳熟能詳了。github
可是對於不少開發者而言,ES6 中的 export
、import
,nodejs
中的 require
、exports.xx
、module.exports
到底有什麼區別?爲何又有 CommonJS
,又有 AMD
,CMD
,UMD
?區別是什麼?甚至咱們在編寫 ts 文件的時候,還須要在配置文件裏面說明什麼模塊方式,在項目中使用的時候,咱們又是否真正知道,你用的究竟是基於哪種規範的模塊化?web
本文對你寫代碼沒有一點幫助,可是若是你還對上述的問題存有疑惑或者想了解JavaScript 模塊化的前世古今,那麼咱們開始吧~
公衆號回覆【xmind2】獲取源文件
模塊化的價值
所謂的模塊化,粗俗的講,就是把一大坨代碼,一鏟一鏟分紅一個個小小坨。固然,這種分割也必須是合理的,以便於你增減或者修改功能,而且不會影響總體系統的穩定性。
我的認爲模塊化具備如下幾個好處:
-
可維護性,每個模塊都是獨立的。良好的設計可以極大的下降項目的耦合度。以便於其能獨立於別的功能被整改。至少維護一個獨立的功能模塊,比維護一坨凌亂的代碼要容易不少。 -
減小全局變量污染,前端開發的初期,咱們都在爲全局變量而頭疼,由於常常會觸發一些難以排查且非技術性的 bug。當一些無關的代碼一不當心重名了全局變量,咱們就會遇到煩人的「命名空間污染」的問題。在模塊化規範沒有肯定以前,其實咱們都在極力的避免於此。(後文會介紹) -
可複用性,前端模塊功能的封裝,極大的提升了代碼的可複用性。這點應該就不用詳細說明了。想一想從 npm
上找package
的時候,是在幹啥? -
方便管理依賴關係,在模塊化規範沒有徹底肯定的時候,模塊之間相互依賴的關係很是的模糊,徹底取決於 js 文件引入的順序。粗俗!絲毫沒有技術含量,不只依賴模糊且難以維護。
原始模塊化
對於某一工程做業或者行爲進行定性的信息規定。主要是由於沒法精準定量而造成的標準,因此,被稱爲規範。在模塊化尚未規範肯定的時候,咱們都稱之爲原始模塊化。
函數封裝
回到咱們剛剛說的模塊的定義,模塊就是一個實現特定功能的文件 or 代碼塊(這是我本身給定義的)。專業定義是,在程序設計中,爲完成某一功能所需的一段程序或子程序;或指能由編譯程序、裝配程序等處理的獨立程序單位;或指大型軟件系統的一部分。而函數的一個功能就是實現特定邏輯的一組語句打包。而且 JavaScript 的做用域就是基於函數的。因此最原始之處,函數必然是做爲模塊化的第一步。
基本語法
//函數1
function fn1(){
//statement
}
//函數2
function fn2(){
//statement
}
優勢
-
有必定的功能隔離和封裝...
缺點
-
污染了全局變量 -
模塊之間的關係模糊
對象封裝
其實就是把變量名塞的深一點。。。
基本語法
let module1 = {
let tag : 1,
let name:'module1',
fun1(){
console.log('this is fun1')
},
fun2(){
console.log('this is fun2')
}
}
咱們在使用的時候呢,就直接
module1.fun2();
優勢
-
必定程度上優化了命名衝突,下降了全局變量污染的風險 -
有必定的模塊封裝和隔離,而且還能夠進一步語義化一些
缺點
-
並無實質上改變命名衝突的問題 -
外部能夠隨意修改內部成員變量,仍是容易產生意外風險
IIFE
IIFE
就是當即執行函數,咱們能夠經過匿名閉包的形式來實現模塊化
基本語法
let global = 'Hello, I am a global variable :)';
(function () {
// 在函數的做用域中下面的變量是私有的
const myGrades = [93, 95, 88, 0, 55, 91];
let average = function() {
let total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return 'Your average grade is ' + total / myGrades.length + '.';
}
let failing = function(){
let failingGrades = myGrades.filter(function(item) {
return item < 70;});
return 'You failed ' + failingGrades.length + ' times.';
}
console.log(failing());
console.log(global);
}());
// 控制檯顯示:'You failed 2 times.'
// 控制檯顯示:'Hello, I am a global variable :)'
這種方法的好處在於,你能夠在函數內部使用局部變量,而不會意外覆蓋同名全局變量,但仍然可以訪問到全局變量
相似如上的 IIFE
,還有很是多的演進寫法
好比引入依賴:
// module.js文件
(function(window, $) {
let data = 'www.baidu.com'
//操做數據的函數
function foo() {
//用於暴露有函數
console.log(`foo() ${data}`)
$('body').css('background', 'red')
}
function bar() {
//用於暴露有函數
console.log(`bar() ${data}`)
otherFun() //內部調用
}
function otherFun() {
//內部私有的函數
console.log('otherFun()')
}
//暴露行爲
window.myModule = { foo, bar }
})(window, jQuery)
// index.html文件
<!-- 引入的js必須有必定順序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
還有一種所謂的揭示模塊模式 Revealing module pattern
var myGradesCalculate = (function () {
// 在函數的做用域中下面的變量是私有的
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'Your average grade is ' + total / myGrades.length + '.';
};
var failing = function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return 'You failed ' + failingGrades.length + ' times.';
};
// 將公有指針指向私有方法
return {
average: average,
failing: failing
}
})();
myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
這和咱們以前的實現方法很是相近,除了它會確保,在全部的變量和方法暴露以前都會保持私有.
優勢
-
實現了基本的封裝 -
只暴露對外的方法操做,有了 public
和private
的概念
缺點
-
模塊依賴關係模糊
CommonJS
上述的全部解決方案都有一個共同點:使用單個全局變量來把全部的代碼包含在一個函數內,由此來建立私有的命名空間和閉包做用域。
雖然每種方法都比較有效,但也都有各自的短板。
隨着大前端時代的到來,常見的 JavaScript 模塊規範也就有了:CommonJS
、AMD
、CMD
、UMD
、ES6
原生。
基本介紹
CommonJS
是 JavaScript 的一個模塊化規範,主要用於服務端Nodejs 中,固然,經過轉換打包,也能夠運行在瀏覽器端。畢竟服務端加載的模塊都是存放於本地磁盤中,因此加載起來比較快,不須要考慮異步方式。
根據規範,每個文件既是一個模塊,其內部定義的變量是屬於這個模塊的,不會污染全局變量。
CommonJS
的核心思想是經過 require
方法來同步加載所依賴的模塊,而後經過 exports
或者 module.exprots
來導出對外暴露的接口。
模塊定義
CommonJS
的規範說明,一個單獨的文件就是一個模塊,也就是一個單獨的做用域。而且模塊只有一個出口,module.exports
/exports.xxx
// lib/math.js
const NAME='Nealayng';
module.exports.author = NAME;
module.exports.add = (a,b)=> a+b;
加載模塊
加載模塊使用 require
方法,該方法讀取文件而且執行,返回文件中 module.exports
對象
// main.js
const mathLib = require('./lib/math');
console.log(mathLib.author);//Nealyang
console.log(mathLib.add(1,2));// 3
在瀏覽器中使用 CommonJS
因爲瀏覽器不支持 CommonJS
規範,由於其根本沒有 module
、exports
、require
等變量,若是要使用,則必須轉換格式。Browserify是目前最經常使用的CommonJS格式轉換的工具,咱們能夠經過安裝browserify
來對其進行轉換.可是咱們仍然須要注意,因爲 CommonJS 的規範是阻塞式加載,而且模塊文件存放在服務器端,可能會出現假死的等待狀態。
npm i browserify -g
而後使用以下命令
browserify main.js -o js/bundle/main.js
而後在 HTML 中引入使用便可。
有一說一,在瀏覽器中使用 CommonJS 的規範去加載模塊,真的不是很方便。若是必定要使用,咱們可使用browserify編譯打包,也可使用require1k,直接在瀏覽器上運行便可。
特色
-
以文件爲一個單元模塊,代碼運行在模塊做用域內,不會污染全局變量 -
同步加載模塊,在服務端直接讀取本地磁盤沒問題,不太適用於瀏覽器 -
模塊能夠加載屢次,可是隻會在第一次加載時運行,而後在加載,就是讀取的緩存文件。需清理緩存後纔可再次讀取文件內容 -
模塊加載的順序,按照其在代碼中出現的順序 -
導出的是值的拷貝,這一點和 ES6 有着很大的不一樣(後面會介紹到)
補充知識點
其實在 nodejs 中模塊的實現並不是徹底按照 CommonJS 的規範來的,而是進行了取捨。
Node 中,一個文件是一個模塊->module
源碼定義以下:
function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
this.exports = {};
this.parent = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
//實例化一個模塊
var module = new Module(filename, parent);
CommonJS 的一個模塊,就是一個腳本文件。require命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象。
{
id: '...',
exports: { ... },
loaded: true,
...
}
上面代碼就是 Node 內部加載模塊後生成的一個對象。該對象的id屬性是模塊名,exports屬性是模塊輸出的各個接口,loaded屬性是一個布爾值,表示該模塊的腳本是否執行完畢。其餘還有不少屬性,這裏都省略不介紹了。
之後須要用到這個模塊的時候,就會到exports屬性上面取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。也就是說,CommonJS 模塊不管加載多少次,都只會在第一次加載時運行一次,之後再加載,就返回第一次運行的結果,除非手動清除系統緩存。
再去深究具體的實現細節。。那就。。。下一篇分享吧~
AMD
Asynchronous Module Definition:異步模塊定義。
也就是解決咱們上面說的 CommonJS 在瀏覽器端致命的問題:假死。
介紹
CommonJS規範加載模塊是同步的,也就是說,只有加載完成,才能執行後面的操做。AMD規範則是異步加載模塊,容許指定回調函數。
因爲其並不是原生 js 所支持的那種寫法。因此使用 AMD 規範開發的時候就須要大名鼎鼎的函數庫 require.js
的支持了。
require.js
https://github.com/requirejs/requirejs
關於 require.js 的更詳細使用說明能夠參考官網 api:https://requirejs.org/docs/api.html
require.js
主要解決兩個問題:
-
異步加載模塊 -
模塊之間依賴模糊
定義模塊
define(id,[dependence],callback)
-
id
,一個可選參數,說白了就是給模塊取個名字,可是倒是模塊的惟一標識。若是沒有提供則取腳本的文件名 -
dependence
,以來的模塊數組 -
callback
,工廠方法,模塊初始化的一些操做。若是是函數,應該只被執行一次。若是是對象,則爲模塊的輸出值
使用模塊
require([moduleName],callback);
-
moduleName
,以來的模塊數組 -
callback
,即爲依賴模塊加載成功以後執行的回調函數(前端異步的通用解決方案),
data-main
<script src="scripts/require.js" data-main="scripts/app.js"></script>
data-main
指定入口文件,好比這裏指定 scripts
下的 app.js
文件,那麼只有直接或者間接與app.js
有依賴關係的模塊纔會被插入到html中。
require.config
經過這個函數能夠對requirejs
進行靈活的配置,其參數爲一個配置對象,配置項及含義以下:
-
baseUrl
——用於加載模塊的根路徑。 -
paths
——用於映射不存在根路徑下面的模塊路徑。 -
shims
——配置在腳本/模塊外面並無使用RequireJS的函數依賴而且初始化函數。假設underscore並無使用RequireJS
定義,可是你仍是想經過RequireJS來使用它,那麼你就須要在配置中把它定義爲一個shim -
deps
——加載依賴關係數組
require.config({
//默認狀況下從這個文件開始拉去取資源
baseUrl:'scripts/app',
//若是你的依賴模塊以pb頭,會從scripts/pb加載模塊。
paths:{
pb:'../pb'
},
// load backbone as a shim,所謂就是將沒有采用requirejs方式定義
//模塊的東西轉變爲requirejs模塊
shim:{
'backbone':{
deps:['underscore'],
exports:'Backbone'
}
}
});
Demo 演示
-
建立項目
|-js
|-libs
|-require.js
|-modules
|-article.js
|-user.js
|-main.js
|-index.html
-
定義模塊
// user.js文件
// 定義沒有依賴的模塊
define(function() {
let author = 'Nealyang'
function getAuthor() {
return author.toUpperCase()
}
return { getAuthor } // 暴露模塊
})
//article.js文件
// 定義有依賴的模塊
define(['user'], function(user) {
let name = 'THE LAST TIME'
function consoleMsg() {
console.log(`${name} by ${user.getAuthor()}`);
}
// 暴露模塊
return { consoleMsg }
})
// main.js
(function() {
require.config({
baseUrl: 'js/', //基本路徑 出發點在根目錄下
paths: {
//映射: 模塊標識名: 路徑
article: './modules/article', //此處不能寫成article.js,會報錯
user: './modules/user'
}
})
require(['article'], function(alerter) {
article.consoleMsg()
})
})()
// index.html文件
<!DOCTYPE html>
<html>
<head>
<title>Modular Demo</title>
</head>
<body>
<!-- 引入require.js並指定js主文件的入口 -->
<script data-main="js/main" src="js/libs/require.js"></script>
</body>
</html>
若是咱們須要引入第三方庫,則須要在 main.js 文件中引入
(function() {
require.config({
baseUrl: 'js/',
paths: {
article: './modules/article',
user: './modules/user',
// 第三方庫模塊
jquery: './libs/jquery-1.10.1' //注意:寫成jQuery會報錯
}
})
require(['article'], function(alerter) {
article.consoleMsg()
})
})()
特色
-
異步加載模塊,不會形成因網絡問題而出現的假死裝填 -
顯式地列出其依賴關係,並以函數(定義此模塊的那個函數)參數的形式將這些依賴進行注入 -
在模塊開始時,加載全部所需依賴
關於 require.js 的使用,仔細看文檔,其實仍是有不少知識點的。可是鑑於咱們着實如今使用很少(我也不熟),因此這裏也就參考網上優秀文章和本身實踐,拋磚引玉。
CMD
基本介紹
CMD是阿里的玉伯提出來的(大神的成長故事可在公衆號回覆【大佬】),js 的函數爲sea.js
,它和 AMD 其實很是的類似,文件即爲模塊,可是其最主要的區別是實現了按需加載。推崇依賴就近的原則,模塊延遲執行,而 AMD 所依賴模塊式提早執行(requireJS 2.0
後也改成了延遲執行)
//AMD
define(['./a','./b'], function (a, b) {
//依賴一開始就寫好
a.test();
b.test();
});
//CMD
define(function (requie, exports, module) {
//依賴能夠就近書寫
var a = require('./a');
a.test();
...
//按需加載
if (status) {
var b = requie('./b');
b.test();
}
});
SeaJs
https://github.com/seajs/seajs
https://seajs.github.io/seajs/docs/
準確的說 CMD
是 SeaJS
在推廣過程當中對模塊定義的規範化產物。
也能夠說SeaJS
是一個遵循 CMD
規範的 JavaScript
模塊加載框架,能夠實現 JavaScript 的 CMD 模塊化開發方式。
SeaJS
只是實現 JavaScript的模塊化和按需加載,並未擴展 JavaScript 語言自己。SeaJS
的主要目的是讓開發人員更加專一於代碼自己,從繁重的 JavaScript 文件以及對象依賴處理中解放出來。
絕不誇張的說,咱們如今詳情頁就是 SeaJS+Kissy。。。(即將升級)
Seajs
追求簡單、天然的代碼書寫和組織方式,具備以下核心特性:
-
簡單友好的模塊定義規範: Sea.js
遵循CMD
規範,能夠像 Node.js 通常書寫模塊代碼。 -
天然直觀的代碼組織方式:依賴的自動加載、配置的簡潔清晰,可讓咱們更多地享受編碼的樂趣。
Sea.js 還提供經常使用插件,很是有助於開發調試和性能優化,並具備豐富的可擴展接口。
Demo 演示
examples/
|-- sea-modules 存放 seajs、jquery 等文件,這也是模塊的部署目錄
|-- static 存放各個項目的 js、css 文件
| |-- hello
| |-- lucky
| `-- todo
`-- app 存放 html 等文件
|-- hello.html
|-- lucky.html
`-- todo.html
咱們從 hello.html 入手,來瞧瞧使用 Sea.js 如何組織代碼。
在 hello.html 頁尾,經過 script 引入 sea.js 後,有一段配置代碼
// seajs 的簡單配置
seajs.config({
base: "../sea-modules/",
alias: {
"jquery": "jquery/jquery/1.10.1/jquery.js"
}
})
// 加載入口模塊
seajs.use("../static/hello/src/main")
sea.js 在下載完成後,會自動加載入口模塊。頁面中的代碼就這麼簡單。
這個小遊戲有兩個模塊 spinning.js 和 main.js,遵循統一的寫法:
// 全部模塊都經過 define 來定義
define(function(require, exports, module) {
// 經過 require 引入依賴
var $ = require('jquery');
var Spinning = require('./spinning');
// 經過 exports 對外提供接口
exports.doSomething = ...
// 或者經過 module.exports 提供整個接口
module.exports = ...
});
上面就是 Sea.js 推薦的 CMD 模塊書寫格式。若是你有使用過 Node.js,一切都很天然。
以上實例,來源於官網 Example。更多 Demo 查看:https://github.com/seajs/examples
特色
-
相對天然的依賴聲明風格,且社區不錯 -
文件即模塊 -
模塊按需加載。 -
推崇依賴就近的原則,模塊延遲執行
UMD
UMD 其實我我的仍是以爲很是。。。。不喜歡的。ifElse
就 universal
了。。。。
基本介紹
UMD
是 AMD
和 CommonJS
的綜合產物。如上所說,AMD
的用武之地是瀏覽器,非阻塞式加載。CommonJS 主要用於服務端 Nodejs 中使用。因此人們就想到了一個通用的模式UMD
(universal module definition)。來解決跨平臺的問題。
沒錯!就是 ifElse
的寫法。
核心思想就是:先判斷是否支持Node.js的模塊(exports
)是否存在,存在則使用Node.js模塊模式。
在判斷是否支持AMD(define
是否存在),存在則使用AMD
方式加載模塊。
常規用法
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
//module ...
});
關於 UMD 更多的example 可移步github:https://github.com/umdjs/umd
ES6
若是你一直讀到如今,那麼恭喜你,咱們開始介紹咱們最新的模塊化了!
經過上面的介紹咱們知道,要麼模塊化依賴環境,要麼須要引入額外的類庫。說到底就是社區找到的一種妥協方案而後獲得了你們的承認。可是歸根結底不是官方呀。終於,ECMAScript 官宣了模塊化的支持,真正的規範。
基本介紹
在ES6中,咱們可使用 import
關鍵字引入模塊,經過 export
關鍵字導出模塊,功能較之於前幾個方案更爲強大,也是咱們所推崇的,可是因爲ES6目前沒法在全部瀏覽器中執行,因此,咱們還需經過babel將不被支持的import
編譯爲當前受到普遍支持的 require
。
ES6 的模塊化汲取了 CommonJS
和AMD
的優勢,擁有簡潔的語法和異步的支持。而且寫法也和 CommonJS 很是的類似。
關於 ES6 模塊的基本用法相比你們都比較熟悉了。這裏咱們主要和 CommonJS 對比學習。
與 CommonJS 的差別
兩大差別:
-
CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。 -
CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
值拷貝&值引用
// lib/counter.js
var counter = 1;
function increment() {
counter++;
}
function decrement() {
counter--;
}
module.exports = {
counter: counter,
increment: increment,
decrement: decrement
};
// src/main.js
var counter = require('../../lib/counter');
counter.increment();
console.log(counter.counter); // 1
在 main.js 當中的實例是和本來模塊徹底不相干的。這也就解釋了爲何調用了 counter.increment() 以後仍然返回1。由於咱們引入的 counter 變量和模塊裏的是兩個不一樣的實例。
因此調用 counter.increment() 方法只會改變模塊中的 counter .想要修改引入的 counter 只有手動一下啦:
counter.counter++;
console.log(counter.counter); // 2
而經過 import 語句,能夠引入實時只讀的模塊:
// lib/counter.js
export let counter = 1;
export function increment() {
counter++;
}
export function decrement() {
counter--;
}
// src/main.js
import * as counter from '../../counter';
console.log(counter.counter); // 1
counter.increment();
console.log(counter.counter); // 2
加載 & 編譯
由於 CommonJS
加載的是一個對象(module.exports
),對象只有在有腳本運行的時候才能生成。而 ES6 模塊不是一個對象,只是一個靜態的定義。在代碼解析階段就會生成。
ES6 模塊是編譯時輸出接口,所以有以下2個特色:
-
import 命令會被 JS 引擎靜態分析,優先於模塊內的其餘內容執行 -
export 命令會有變量聲明提高的效果,因此import 和 export 命令在模塊中的位置並不影響程序的輸出。
// a.js
console.log('a.js')
import { foo } from './b';
// b.js
export let foo = 1;
console.log('b.js 先執行');
// 執行結果:
// b.js 先執行
// a.js
// a.js
import { foo } from './b';
console.log('a.js');
export const bar = 1;
export const bar2 = () => {
console.log('bar2');
}
export function bar3() {
console.log('bar3');
}
// b.js
export let foo = 1;
import * as a from './a';
console.log(a);
// 執行結果:
// { bar: undefined, bar2: undefined, bar3: [Function: bar3] }
// a.js
循環加載的差別
「循環加載」(circular dependency)指的是,a腳本的執行依賴b腳本,而b腳本的執行又依賴a腳本。
// a.js
var b = require('b');
// b.js
var a = require('a');
循環加載若是處理很差,還可能致使遞歸加載,使得程序沒法執行,所以應該避免出現。
在 CommonJS 中,腳本代碼在 require
的時候,就會所有執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。
// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');
// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執行完畢');
// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
輸出結果爲:
在 b.js 之中,a.done = false
b.js 執行完畢
在 a.js 之中,b.done = true
a.js 執行完畢
在 main.js 之中, a.done=true, b.done=true
從上面咱們能夠看出:
-
在 b.js
之中,a.js
沒有執行完畢,只執行了第一行。 -
main.js
執行到第二行時,不會再次執行b.js
,而是輸出緩存的b.js
的執行結果,即它的第四行
ES6 處理「循環加載」與 CommonJS 有本質的不一樣**。ES6 模塊是動態引用**,若是使用import從一個模塊加載變量(即import foo from 'foo'),那些變量不會被緩存,而是成爲一個指向被加載模塊的引用,須要開發者本身保證,真正取值的時候可以取到值。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
運行結果以下:
b.mjs
ReferenceError: foo is not defined
上面代碼中,執行a.mjs之後會報錯,foo變量未定義.
具體的執行結果以下:
-
執行a.mjs之後,引擎發現它加載了b.mjs,所以會優先執行b.mjs,而後再執行a.mjs -
執行b.mjs的時候,已知它從a.mjs輸入了foo接口,這時不會去執行a.mjs,而是認爲這個接口已經存在了,繼續往下執行。 -
執行到第三行console.log(foo)的時候,才發現這個接口根本沒定義,所以報錯。
解決這個問題的方法,就是讓b.mjs運行的時候,foo已經有定義了。這能夠經過將foo寫成函數來解決。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
最後執行結果爲:
b.mjs
foo
a.mjs
bar
特色
-
每個模塊加載屢次, JS只執行一次, 若是下次再去加載同目錄下同文件,直接從內存中讀取。一個模塊就是一個單例,或者說就是一個對象 -
代碼是在模塊做用域之中運行,而不是在全局做用域運行。模塊內部的頂層變量,外部不可見。不會污染全局做用域; -
模塊腳本自動採用嚴格模式,無論有沒有聲明use strict -
模塊之中,可使用import命令加載其餘模塊(.js後綴不可省略,須要提供絕對 URL 或相對 URL),也可使用export命令輸出對外接口 -
模塊之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模塊頂層使用this關鍵字,是無心義的
關於 ES6 詳細的模塊的介紹,強烈推薦阮一峯的 ES6 入門和深刻理解 ES6 一書
參考文獻
-
30分鐘學會前端模塊化開發 -
阮一峯 ES6 -
前端模塊化詳解(完整版) -
理解CommonJS、AMD、CMD三種規範 -
前端模塊化開發那點歷史 -
JavaScript 模塊化入門Ⅰ:理解模塊 -
前端模塊化開發的價值
完
本文分享自微信公衆號 - 編程微刊(wangxiaoting678)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。