【THE LAST TIME】深刻淺出 JavaScript 模塊化

前言

The last time, I have learned

【THE LAST TIME】一直是我想寫的一個系列,旨在厚積薄發,重溫前端。javascript

也是對本身的查缺補漏和技術分享。css

歡迎你們多多評論指點吐槽。html

系列文章均首發於公衆號【全棧前端精選】,筆者文章集合詳見GitHub 地址: Nealyang/personalBlog。目錄和發文順序皆爲暫定

隨着互聯網的發展,前端開發也變的愈來愈複雜,從一開始的表單驗證到如今動不動上千上萬行代碼的項目開發,團隊協做就是咱們不可避免的工做方式,爲了更好地管理功能邏輯,模塊化的概念也就漸漸產生了。前端

好的書籍📚會分章節,好的代碼得分模塊。java

JavaScript 在早期的設計中就沒有模塊、包甚至類的概念,雖然 ES6 中有了 class 關鍵字,那也只是個語法糖。隨意隨着項目複雜度的增長,開發者必然須要模擬類的功能,來隔離、封裝、組織複雜的 JavaScript 代碼,而這種封裝和隔離,也被被咱們稱之爲模塊化。node

模塊就是一個實現特定功能的文件 or 代碼塊。隨着前端工程體系建設的愈發成熟,或許模塊化的概念已經在前端圈子裏已經耳熟能詳了。jquery

可是對於不少開發者而言,ES6 中的 exportimportnodejs 中的 requireexports.xxmodule.exports到底有什麼區別?爲何又有 CommonJS,又有 AMDCMDUMD?區別是什麼?甚至咱們在編寫 ts 文件的時候,還須要在配置文件裏面說明什麼模塊方式,在項目中使用的時候,咱們又是否真正知道,你用的究竟是基於哪種規範的模塊化?git

本文對你寫代碼沒有一點幫助,可是若是你還對上述的問題存有疑惑或者想了解JavaScript 模塊化的前世古今,那麼咱們開始吧~es6

公衆號回覆【xmind2】獲取源文件

模塊化的價值

所謂的模塊化,粗俗的講,就是把一大坨代碼,一鏟一鏟分紅一個個小小坨。固然,這種分割也必須是合理的,以便於你增減或者修改功能,而且不會影響總體系統的穩定性。github

我的認爲模塊化具備如下幾個好處:

  • 可維護性,每個模塊都是獨立的。良好的設計可以極大的下降項目的耦合度。以便於其能獨立於別的功能被整改。至少維護一個獨立的功能模塊,比維護一坨凌亂的代碼要容易不少。
  • 減小全局變量污染,前端開發的初期,咱們都在爲全局變量而頭疼,由於常常會觸發一些難以排查且非技術性的 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.'

這和咱們以前的實現方法很是相近,除了它會確保,在全部的變量和方法暴露以前都會保持私有.

優勢

  • 實現了基本的封裝
  • 只暴露對外的方法操做,有了 publicprivate 的概念

缺點

  • 模塊依賴關係模糊

CommonJS

上述的全部解決方案都有一個共同點:使用單個全局變量來把全部的代碼包含在一個函數內,由此來建立私有的命名空間和閉包做用域。

雖然每種方法都比較有效,但也都有各自的短板。

隨着大前端時代的到來,常見的 JavaScript 模塊規範也就有了:CommonJSAMDCMDUMDES6 原生。

基本介紹

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 規範,由於其根本沒有 moduleexportsrequire 等變量,若是要使用,則必須轉換格式。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/...

關於 require.js 的更詳細使用說明能夠參考官網 api:https://requirejs.org/docs/ap...

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...

準確的說 CMDSeaJS 在推廣過程當中對模塊定義的規範化產物。

也能夠說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 其實我我的仍是以爲很是。。。。不喜歡的。ifElseuniversal 了。。。。

基本介紹

UMDAMDCommonJS 的綜合產物。如上所說,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 的模塊化汲取了 CommonJSAMD 的優勢,擁有簡潔的語法和異步的支持。而且寫法也和 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 一書

參考文獻

學習交流

  • 關注公衆號【全棧前端精選】,每日獲取好文推薦
  • 添加微信號:is_Nealyang(備註來源) ,入羣交流
公衆號【全棧前端精選】 我的微信【is_Nealyang】
相關文章
相關標籤/搜索