模塊化审残、CommonJS 規(guī)范和 ES6 模塊規(guī)范以及簡單手寫 CommonJS 的 require

無模塊化

簡單的將所有的 js 文件統(tǒng)統(tǒng)放在一起麻掸,然后通過 <script> 標簽引入酥夭。

  • 優(yōu)點:
    • 相比于使用一個js文件,這種多個js文件實現(xiàn)最簡單的模塊化的思想是進步的脊奋。
  • 缺點:
    • 污染全局作用域熬北。因為每一個模塊都是暴露在全局的,簡單的使用诚隙,會導(dǎo)致全局變量命名沖突讶隐,當(dāng)然,我們也可以使用命名空間的方式來解決久又。
    • 對于大型項目巫延,各種js很多,開發(fā)人員必須手動解決模塊和代碼庫的依賴關(guān)系地消,后期維護成本較高炉峰。
    • 依賴關(guān)系不明顯,不利于維護脉执。 比如 main.js 需要使用 jquery疼阔,但是,從上面的文件中适瓦,我們是看不出來的竿开,如果 jquery 忘記了,那么就會報錯玻熙。
  <!-- 頁面內(nèi)嵌的腳本 -->
  <script type="application/javascript">
  // module code
  </script>

  <!-- 外部腳本 -->
  <script type="application/javascript" src="path/to/myModule.js">
  </script>

上面代碼中否彩,由于瀏覽器腳本的默認語言是 JavaScript,因此 type="application/javascript" 可以省略嗦随。

  • 默認情況下列荔,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到 <script> 標簽就會停下來枚尼,等到執(zhí)行完腳本贴浙,再繼續(xù)向下渲染。如果是外部腳本署恍,還必須加入腳本下載的時間崎溃。如果腳本體積很大,下載和執(zhí)行的時間就會很長盯质,因此造成瀏覽器堵塞袁串,用戶會感覺到瀏覽器“卡死”了,沒有任何響應(yīng)呼巷。
  • 瀏覽器允許腳本異步加載囱修,下面就是兩種異步加載的語法。
    <script src="path/to/myModule.js" defer></script>
    <script src="path/to/myModule.js" async></script>

上面代碼中王悍,<script> 標簽打開 defer 或 async 屬性破镰,腳本就會異步加載。渲染引擎遇到這一行命令压储,就會開始下載外部腳本鲜漩,但不會等它下載和執(zhí)行,而是直接執(zhí)行后面的命令渠脉。

  • defer 與 async 的區(qū)別是:
    • defer 要等到整個頁面在內(nèi)存中正常渲染結(jié)束(DOM 結(jié)構(gòu)完全生成宇整,以及其他腳本執(zhí)行完成),才會執(zhí)行
    • async 一旦下載完芋膘,渲染引擎就會中斷渲染鳞青,執(zhí)行這個腳本以后,再繼續(xù)渲染
    • 一句話为朋,defer 是 “渲染完再執(zhí)行”臂拓,async 是 “下載完就執(zhí)行”
    • 如果有多個 defer 腳本,會按照它們在頁面出現(xiàn)的順序加載习寸,而多個 async 腳本是不能保證加載順序的

CommonJS 規(guī)范

1. 概述

  • 是一個 JavaScript 模塊化的規(guī)范胶惰, Nodejs 環(huán)境所使用的模塊系統(tǒng)就是基于 CommonJS 規(guī)范實現(xiàn)的,我們現(xiàn)在所說的 CommonJS 規(guī)范也大多是指 Node 的模塊系統(tǒng)霞溪,前端的 webpack 也是對 CommonJS 原生支持孵滞。
  • CommonJS 規(guī)范中捆,每一個文件就是一個模塊,其內(nèi)部定義的變量是屬于這個模塊的坊饶,不會對外暴露泄伪,也就是說不會污染全局變量。
  • 有四個重要的環(huán)境變量為模塊化的實現(xiàn)提供支持:module匿级、exports蟋滴、requireglobal痘绎。實際使用時津函,用 module.exports 定義當(dāng)前模塊對外輸出的接口(不推薦直接用 exports ),用 require 加載模塊孤页。
    // example.js
    var x = 5;
    var addX = function (value) {
        return value + x;
    };

上面代碼中尔苦,變量 x 和函數(shù) addX,是當(dāng)前文件 example.js 私有的散庶,其他文件不可見蕉堰。

  • 如果想在多個文件分享變量,必須定義為 global 對象的屬性悲龟。
    global.warning = true

上面代碼的 warning 變量屋讶,可以被所有文件讀取。當(dāng)然须教,這樣寫法是不推薦的皿渗。

  • CommonJS 規(guī)范規(guī)定,每個模塊內(nèi)部轻腺,module 變量代表當(dāng)前模塊乐疆。這個變量是一個對象,它的 exports 屬性(即module.exports)是對外的接口贬养。加載某個模塊挤土,其實是加載該模塊的 module.exports 屬性。
    // example.js
    var x = 5;
    var addX = function (value) {
        return value + x;
    };
    module.exports = {
        x: x,
        addX: addX
    };

上面代碼通過 module.exports 輸出變量 x 和函數(shù) addX误算。

  • require 方法用于加載模塊
    var example = require('./example.js');

    console.log(example.x); // 5
    console.log(example.addX(1)); // 6
  • CommonJS 規(guī)范特點

    • 所有代碼都運行在模塊作用域仰美,不會污染全局作用域
    • CommonJS 模塊可以多次加載,但是只會在第一次加載時運行一次儿礼,然后運行結(jié)果就被緩存了咖杂,以后再加載,就直接讀取緩存結(jié)果蚊夫。要想讓模塊再次運行诉字,必須清除緩存
    • CommonJS 模塊加載的順序,按照其在代碼中出現(xiàn)的順序
    • 由于 Node.js 主要用于服務(wù)器編程,模塊文件一般都已經(jīng)存在于本地硬盤壤圃,所以加載起來比較快陵霉,不用考慮非同步加載的方式,所以 CommonJS 規(guī)范比較適用
  • 優(yōu)點:

    • CommonJS 規(guī)范在服務(wù)器端率先完成了 JavaScript 的模塊化伍绳,解決了依賴撩匕、全局變量污染的問題,這也是 js 運行在服務(wù)器端的必要條件墨叛。
  • 缺點:

    • 由于 CommonJS 是同步加載模塊的,在服務(wù)器端模蜡,文件都是保存在硬盤上漠趁,所以同步加載沒有問題,但是對于瀏覽器端忍疾,需要將文件從服務(wù)器端請求過來闯传,那么同步加載就不適用了,所以卤妒,CommonJS 是不適用于瀏覽器端的甥绿。

2. module 對象

  • Node 內(nèi)部提供一個 Module 構(gòu)建函數(shù)。所有模塊都是 Module 的實例则披。
  function Module(id, parent) {
    this.id = id;
    this.exports = {};
    this.parent = parent;
    // ...
  • 每個模塊內(nèi)部共缕,都有一個 module 對象,代表當(dāng)前模塊士复。它有以下屬性图谷。
  module.id 模塊的識別符,通常是帶有絕對路徑的模塊文件名阱洪。
  module.filename 模塊的文件名便贵,帶有絕對路徑。
  module.loaded 返回一個布爾值冗荸,表示模塊是否已經(jīng)完成加載承璃。
  module.parent 返回一個對象,表示調(diào)用該模塊的模塊蚌本。
  module.children 返回一個數(shù)組盔粹,表示該模塊要用到的其他模塊。
  module.exports 表示模塊對外輸出的值魂毁。
  • 如果在命令行下調(diào)用某個模塊玻佩,比如 node something.js ,那么 module.parent 就是 null 席楚。如果是在腳本之中調(diào)用咬崔,比如 require('./something.js') ,那么 module.parent 就是調(diào)用它的模塊。利用這一點垮斯,可以判斷當(dāng)前模塊是否為入口腳本郎仆。
  if (!module.parent) {
    // ran with `node something.js`
    app.listen(8088, function() {
      console.log('app listening on port 8088');
    });
  } else {
    // used with `require('/.something.js')`
    module.exports = app;
  }
  • module.exports 屬性表示當(dāng)前模塊對外輸出的接口,其他文件加載該模塊兜蠕,實際上就是讀取 module.exports 變量扰肌。
  var EventEmitter = require('events').EventEmitter;
  module.exports = new EventEmitter();

  setTimeout(function() {
      module.exports.emit('ready');
  }, 1000);

上面模塊會在加載后1秒后,發(fā)出 ready 事件熊杨。其他文件監(jiān)聽該事件曙旭,可以寫成下面這樣。

  var a = require('./a');
  a.on('ready', function() {
    console.log('module a is ready');
  });

3. exports 變量

  • 為了方便晶府,Node 為每個模塊提供一個 exports 變量桂躏,指向 module.exports。這等同在每個模塊頭部川陆,有一行這樣的命令剂习。
  var exports = module.exports;

造成的結(jié)果是,在對外輸出模塊接口時较沪,可以向exports對象添加方法鳞绕。

  exports.area = function (r) {
    return Math.PI * r * r;
  };

  exports.circumference = function (r) {
    return 2 * Math.PI * r;
  };
  • 注意,不能直接將 exports 變量指向一個值尸曼,因為這樣等于切斷了 exports 與 module.exports 的聯(lián)系们何。
  exports.hello = function() {
    return 'hello';
  };

  module.exports = 'Hello world';

上面代碼中,hello 函數(shù)是無法對外輸出的控轿,因為 module.exports 被重新賦值了

  • 這意味著垂蜗,如果一個模塊的對外接口,就是一個單一的值解幽,不能使用 exports 輸出贴见,只能使用 module.exports 輸出,如下。
  module.exports = function (x) { console.log(x); };

4. require 命令

4.1 基本用法

Node 使用 CommonJS 模塊規(guī)范躲株,內(nèi)置的 require 命令用于加載模塊文件

  • require 命令的基本功能是片部,讀入并執(zhí)行一個 JavaScript 文件,然后返回該模塊的 exports 對象霜定。如果沒有發(fā)現(xiàn)指定模塊档悠,會報錯。
  // example.js
  var invisible = function () {
    console.log("invisible");
  }

  exports.message = "hi";

  exports.say = function () {
    console.log(message);
  }

運行以下命令輸出 exports 對象

  var example = require('./example.js');
  example
  // {
  //   message: "hi",
  //   say: [Function]
  // }
  • 如果模塊輸出的是一個函數(shù)望浩,那就不能定義在 exports 對象上面辖所,而要定義在 module.exports 變量上面。
  module.exports = function () {
    console.log("hello world");
  };

  require('./example2.js')();
4.2 加載規(guī)則
  • require 命令用于加載文件磨德,后綴名默認為 .js缘回。
  var foo = require('foo');
  //  等同于
  var foo = require('foo.js');
  • 根據(jù)參數(shù)的不同格式吆视,require命令去不同路徑尋找模塊文件。
    • (1)如果參數(shù)字符串以 / 開頭酥宴,則表示加載的是一個位于絕對路徑的模塊文件啦吧。比如,require('/home/marco/foo.js') 將加載 /home/marco/foo.js拙寡。
    • (2)如果參數(shù)字符串以 ./ 開頭授滓,則表示加載的是一個位于相對路徑(跟當(dāng)前執(zhí)行腳本的位置相比)的模塊文件。比如肆糕,require('./circle') 將加載當(dāng)前腳本同一目錄的 circle.js般堆。
    • (3)如果參數(shù)字符串不以 .// 開頭,則表示加載的是一個默認提供的核心模塊(位于 Node 的系統(tǒng)安裝目錄中)诚啃,或者一個位于各級 node_modules 目錄的已安裝模塊(全局安裝或局部安裝)郁妈。
  • 舉例來說,腳本 /home/user/projects/foo.js 執(zhí)行了 require('bar.js') 命令绍申,Node 會依次搜索以下文件。
    • (4)如果參數(shù)字符串不以 .// 開頭顾彰,而且是一個路徑极阅,比如 require('example-module/path/to/file'),則將先找到 example-module 的位置涨享,然后再以它為參數(shù)筋搏,找到后續(xù)路徑。
    • (5)如果指定的模塊文件沒有發(fā)現(xiàn)厕隧,Node 會嘗試為文件名添加 .js 奔脐、 .json 、 .node 后吁讨,再去搜索髓迎。 .js 件會以文本格式的 JavaScript 腳本文件解析, .json 文件會以 JSON 格式的文本文件解析建丧, .node 文件會以編譯后的二進制文件解析排龄。
    • (6)如果想得到 require 命令加載的確切文件名,使用 require.resolve() 方法翎朱。
  /usr/local/lib/node/bar.js
  /home/user/projects/node_modules/bar.js
  /home/user/node_modules/bar.js
  /home/node_modules/bar.js
  /node_modules/bar.js

這樣設(shè)計的目的是橄维,使得不同的模塊可以將所依賴的模塊本地化

4.3 目錄的加載規(guī)則
  • 通常,我們會把相關(guān)的文件會放在一個目錄里面拴曲,便于組織争舞。這時,最好為該目錄設(shè)置一個入口文件澈灼,讓 require 方法可以通過這個入口文件竞川,加載整個目錄。

在目錄中放置一個 package.json 文件,并且將入口文件寫入 main 字段流译。下面是一個例子逞怨。

  // package.json
  { 
    "name" : "some-library",
    "main" : "./lib/some-library.js"
  }
  • require 發(fā)現(xiàn)參數(shù)字符串指向一個目錄以后,會自動查看該目錄的 package.json 文件福澡,然后加載 main 字段指定的入口文件叠赦。如果 package.json 文件沒有 main 字段,或者根本就沒有 package.json 文件革砸,則會加載該目錄下的 index.js 文件或 index.node 文件除秀。
4.4 模塊的緩存
  • 第一次加載某個模塊時,Node 會緩存該模塊算利。以后再加載該模塊册踩,就直接從緩存取出該模塊的 module.exports 屬性。
 require('./example.js');
 require('./example.js').message = "hello";
 require('./example.js').message
 // "hello"

上面代碼中效拭,連續(xù)三次使用 require 命令暂吉,加載同一個模塊。第二次加載的時候缎患,為輸出的對象添加了一個 message 屬性慕的。但是第三次加載的時候,這個 message 屬性依然存在挤渔,這就證明 require 命令并沒有重新加載模塊文件肮街,而是輸出了緩存。

  • 如果想要多次執(zhí)行某個模塊判导,可以讓該模塊輸出一個函數(shù)嫉父,然后每次 require 這個模塊的時候,重新執(zhí)行一下輸出的函數(shù)眼刃。

所有緩存的模塊保存在 require.cache 之中绕辖,如果想刪除模塊的緩存,可以像下面這樣寫擂红。

  // 刪除指定模塊的緩存
  delete require.cache[moduleName];

  // 刪除所有模塊的緩存
  Object.keys(require.cache).forEach(function(key) {
    delete require.cache[key];
  });
  • 注意引镊,緩存是根據(jù)絕對路徑識別模塊的,如果同樣的模塊名篮条,但是保存在不同的路徑对碌,require 命令還是會重新加載該模塊俱笛。

5. 環(huán)境變量 NODE_PATH

  • Node 執(zhí)行一個腳本時玛痊,會先查看環(huán)境變量 NODE_PATH商玫。它是一組以冒號分隔的絕對路徑。在其他位置找不到指定模塊時伴栓,Node 會去這些路徑查找伦连。

可以將 NODE_PATH 添加到 .bashrc雨饺。

  export NODE_PATH="/usr/local/lib/node"

所以,如果遇到復(fù)雜的相對路徑惑淳,比如下面這樣额港。

  var myModule = require('../../../../lib/myModule');
  • 有兩種解決方法,一是將該文件加入 node_modules 目錄歧焦,二是修改 NODE_PATH 環(huán)境變量移斩,package.json 文件可以采用下面的寫法。
  {
    "name": "node_path",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
      "start": "NODE_PATH=lib node index.js"
    },
    "author": "",
    "license": "ISC"
  }
  • NODE_PATH 是歷史遺留下來的一個路徑解決方案绢馍,通常不應(yīng)該使用向瓷,而應(yīng)該使用 node_modules 目錄機制。
4.6 模塊的循環(huán)加載
  • 如果發(fā)生模塊的循環(huán)加載舰涌,即A加載B猖任,B又加載A,則B將加載A的不完整版本瓷耙。
  // a.js
  exports.x = 'a1';
  console.log('a.js ', require('./b.js').x);
  exports.x = 'a2';

  // b.js
  exports.x = 'b1';
  console.log('b.js ', require('./a.js').x);
  exports.x = 'b2';

  // main.js
  console.log('main.js ', require('./a.js').x);
  console.log('main.js ', require('./b.js').x);

上面代碼是三個 JavaScript 文件朱躺。其中,a.js 加載了 b.js搁痛,而 b.js 又加載 a.js长搀。這時, Node 返回 a.js 的不完整版本落追,所以執(zhí)行結(jié)果如下。

  $ node main.js
  b.js  a1
  a.js  b2
  main.js  a2
  main.js  b2

修改 main.js涯肩,再次加載 a.js 和 b.js轿钠。

    // main.js
    console.log('main.js ', require('./a.js').x);
    console.log('main.js ', require('./b.js').x);
    console.log('main.js ', require('./a.js').x);
    console.log('main.js ', require('./b.js').x);

執(zhí)行上面代碼

  $ node main.js
  b.js  a1
  a.js  b2
  main.js  a2
  main.js  b2
  main.js  a2
  main.js  b2
  • 上面代碼中,第二次加載 a.js 和 b.js 時病苗,會直接從緩存讀取 exports 屬性疗垛,所以 a.js 和 b.js 內(nèi)部的 console.log 語句都不會執(zhí)行了。
4.7 require.main
  • require 方法有一個 main 屬性硫朦,可以用來判斷模塊是直接執(zhí)行贷腕,還是被調(diào)用執(zhí)行。

直接執(zhí)行的時候(node module.js)咬展,require.main 屬性指向模塊本身泽裳。

    require.main === module
    // true

調(diào)用執(zhí)行的時候(通過 require 加載該腳本執(zhí)行),上面的表達式返回 false 破婆。

5. 模塊的加載機制

  • CommonJS 模塊輸出的是一個值的拷貝涮总,一旦輸出一個值,模塊內(nèi)部的變化就影響不到這個值
  • CommonJS 模塊是運行時加載祷舀,加載的是一個對象(即 module.exports 屬性)瀑梗,該對象只有在腳本運行完才會生成
  // lib.js
  var counter = 3;
  function incCounter() {
    counter++;
  }
  module.exports = {
    counter: counter,
    incCounter: incCounter,
  };

上面代碼輸出內(nèi)部變量 counter 和改寫這個變量的內(nèi)部方法 incCounter烹笔。然后,在 main.js 里面加載這個模塊抛丽。

  // main.js
  var com = require('./lib')

  console.log(com.counter) // 3
  com.incCounter()
  console.log(com.counter) // 3

上面代碼說明谤职,lib.js 模塊加載以后,它的內(nèi)部變化就影響不到輸出的 com.counter 了亿鲜。這是因為 com.counter 是一個原始類型的值允蜈,會被緩存。除非寫成一個函數(shù)狡门,才能得到內(nèi)部變動后的值陷寝。

  // lib.js
  var counter = 3;
  function incCounter() {
    counter++;
  }
  module.exports = {
    get counter() {
      return counter
    },
    incCounter: incCounter,
  };

上面代碼中,輸出的 counter 屬性實際上是一個取值器函數(shù)∑淞螅現(xiàn)在再執(zhí)行 main.js 凤跑,就可以正確讀取內(nèi)部變量 counter 的變動了。

  $ node main.js
  3
  4
5.1 require 的內(nèi)部處理流程
  • require 命令是 CommonJS 規(guī)范之中叛复,用來加載其他模塊的命令仔引。它其實不是一個全局命令,而是指向當(dāng)前模塊的 module.require 命令褐奥,而后者又調(diào)用 Node 的內(nèi)部命令 Module._load 咖耘。
  Module._load = function(request, parent, isMain) {
    // 1. 檢查 Module._cache,是否緩存之中有指定模塊
    // 2. 如果緩存之中沒有撬码,就創(chuàng)建一個新的Module實例
    // 3. 將它保存到緩存
    // 4. 使用 module.load() 加載指定的模塊文件儿倒,
    //    讀取文件內(nèi)容之后,使用 module.compile() 執(zhí)行文件代碼
    // 5. 如果加載/解析過程報錯呜笑,就從緩存刪除該模塊
    // 6. 返回該模塊的 module.exports
  };

上面的第 4 步夫否,采用 module.compile() 執(zhí)行指定模塊的腳本,邏輯如下叫胁。

  Module.prototype._compile = function(content, filename) {
    // 1. 生成一個require函數(shù)凰慈,指向module.require
    // 2. 加載其他輔助方法到require
    // 3. 將文件內(nèi)容放到一個函數(shù)之中,該函數(shù)可調(diào)用 require
    // 4. 執(zhí)行該函數(shù)
  };

上面的第 1 步和第 2 步驼鹅, require 函數(shù)及其輔助方法主要如下微谓。

  require(): 加載外部模塊
  require.resolve():將模塊名解析到一個絕對路徑
  require.main:指向主模塊
  require.cache:指向所有緩存的模塊
  require.extensions:根據(jù)文件的后綴名,調(diào)用不同的執(zhí)行函數(shù)

一旦 require 函數(shù)準備完畢输钩,整個所要加載的腳本內(nèi)容豺型,就被放到一個新的函數(shù)之中,這樣可以避免污染全局環(huán)境买乃。該函數(shù)的參數(shù)包括 require触创、module、exports 为牍,以及其他一些參數(shù)哼绑。

  (function (exports, require, module, __filename, __dirname) {
    // YOUR CODE INJECTED HERE!
  });
  • Module._compile 方法是同步執(zhí)行的岩馍,所以 Module._load 要等它執(zhí)行完成,才會向用戶返回 module.exports 的值

CommonJS 規(guī)范 require 方法簡單實現(xiàn)

  • 通過讀取文件內(nèi)容將內(nèi)容包裝到一個自執(zhí)行函數(shù)中抖韩,默認返回 module.exports 做為函數(shù)的結(jié)果蛀恩。
  const a = `function (exports, require, module, __filename, __dirname) {
      let a = 1;
      module.exports = 'hello';
      return module.exports;
  }(exports, require, module, xxxx, xxx)`;
  function Module(id) {
    this.id = id;
    // 代表的是模塊的返回結(jié)果
    this.exports = {};
  }

  Module._cache = {};

  Module.wrapper = [
    `(function(exports, require, module, __filename, __dirname) {`,
    `})`
  ];

  Module._extensions = {
    '.js'(module) {
      let content = fs.readFileSync(module.id, 'utf8');
      content = Module.wrapper[0] + content + Module.wrapper[1];
      // 需要讓函數(shù)字符串變成真正的函數(shù)
      let fn = vm.runInThisContext(content);
      let exports = module.exports; // {}
      let dirname = path.dirname(module.id);
      // 讓包裝的函數(shù)執(zhí)行 require 時會讓包裝的函數(shù)執(zhí)行,并且把this改變
      fn.call(exports, exports, req, module, module.id, dirname);
    },
    '.json'(module) {
      let content = fs.readFileSync(module.id, 'utf8');
      module.exports = JSON.parse(content);
    }
  };

  Module._resolveFilename = function (filename) {
    let absPath = path.resolve(__dirname, filename);
    // 查看路徑是否存在 如果不存在 則增加 .js 或者 .json 后綴
    let isExists = fs.existsSync(absPath);
    if (isExists) {
      return absPath;
    } else {
      let keys = Object.keys(Module._extensions);
      for (let i = 0; i < keys.length; i++) {
        let newPath = absPath + keys[i];
        let flag = fs.existsSync(newPath);
        if (flag) {
          return newPath;
        }
      }
      throw new Error('module not exists');
    }
  };

  Module.prototype.load = function () {
    let extname = path.extname(this.id);
    // module.exports = 'hello'
    Module._extensions[extname](this);
  }

  function req(filename) { // 默認傳入的文件名可能沒有增加后綴茂浮,如果沒有后綴我就嘗試增加.js .json
    // 解析出絕對路徑
    filename = Module._resolveFilename(filename);
    // 創(chuàng)建一個模塊
    // 這里加載前先看一眼 是否加載過了
    let cacheModule = Module._cache[filename]; //  多次引用同一個模塊只運行一次
    if (cacheModule) {
      return cacheModule.exports; // 返回緩存的結(jié)果即可
    }
    let module = new Module(filename);
    Module._cache[filename] = module
    // 加載模塊
    module.load();
    return module.exports;
  };

ES6 加載規(guī)則

  • 瀏覽器加載 ES6 模塊双谆,也使用 <script> 標簽,但是要加入 type="module" 屬性席揽。
  <script type="module" src="./foo.js"></script>
  • 上面代碼在網(wǎng)頁中插入一個模塊 foo.js 顽馋,由于 type 屬性設(shè)為 module ,所以瀏覽器知道這是一個 ES6 模塊幌羞。
  • 瀏覽器對于帶有 type="module" 的 <script> 寸谜,都是異步加載,不會造成堵塞瀏覽器属桦,即等到整個頁面渲染完熊痴,再執(zhí)行模塊腳本,等同于打開了 <script> 標簽的 defer 屬性聂宾。
    <script type="module" src="./foo.js"></script>
    <!-- 等同于 -->
    <script type="module" src="./foo.js" defer></script>
  • 如果網(wǎng)頁有多個 <script type="module"> 果善,它們會按照在頁面出現(xiàn)的順序依次執(zhí)行。
  • <script> 標簽的 async 屬性也可以打開系谐,這時只要加載完成巾陕,渲染引擎就會中斷渲染立即執(zhí)行。執(zhí)行完成后纪他,再恢復(fù)渲染鄙煤。
  <script type="module" src="./foo.js" async></script>
  • 一旦使用了 async 屬性,<script type="module"> 就不會按照在頁面出現(xiàn)的順序執(zhí)行止喷,而是只要該模塊加載完成馆类,就執(zhí)行該模塊混聊。
  • ES6 模塊也允許內(nèi)嵌在網(wǎng)頁中弹谁,語法行為與加載外部腳本完全一致。
  <script type="module">
    import utils from "./utils.js";
    // other code
  </script>

對于外部的模塊腳本(上例是 foo.js )句喜,有幾點需要注意预愤。

  • 代碼是在模塊作用域之中運行,而不是在全局作用域運行咳胃。模塊內(nèi)部的頂層變量植康,外部不可見。
  • 模塊腳本自動采用嚴格模式展懈,不管有沒有聲明 use strict 销睁。
  • 模塊之中供璧,可以使用 import 命令加載其他模塊( .js 后綴不可省略,需要提供絕對 URL 或相對 URL)冻记,也可以使用 export 命令輸出對外接口睡毒。
  • 模塊之中,頂層的 this 關(guān)鍵字返回 undefined 冗栗,而不是指向 window 演顾。也就是說,在模塊頂層使用 this 關(guān)鍵字隅居,是無意義的钠至。
  • 同一個模塊如果加載多次,將只執(zhí)行一次胎源。

示例

  import utils from 'https://example.com/js/utils.js';

  const x = 1;

  console.log(x === window.x); //false
  console.log(this === undefined); // true

利用頂層的 this 等于 undefined 這個語法點棉钧,可以偵測當(dāng)前代碼是否在 ES6 模塊之中。

    const isNotModuleScript = this !== undefined;

ES6 模塊與 CommonJS 模塊的差異

  • ES6 模塊的運行機制與 CommonJS 不一樣乒融。JS 引擎對腳本靜態(tài)分析的時候掰盘,遇到模塊加載命令import,就會生成一個只讀引用赞季。等到腳本真正執(zhí)行時愧捕,再根據(jù)這個只讀引用,到被加載的那個模塊里面去取值申钩。換句話說次绘,ES6 的 import 有點像 Unix 系統(tǒng)的“符號連接”,原始值變了撒遣,import 加載的值也會跟著變邮偎。因此,ES6 模塊是動態(tài)引用义黎,并且不會緩存值禾进,模塊里面的變量綁定其所在的模塊。

以之前 2-CommonJS 規(guī)范中的例子為例

  // lib.js
  export let counter = 3;
  export function incCounter() {
    counter++;
  }

  // main.js
  import { counter, incCounter } from './lib';
  console.log(counter); // 3
  incCounter();
  console.log(counter); // 4

再舉一個例子

  // m1.js
  export var foo = 'bar';
  setTimeout(() => foo = 'baz', 500);

  // m2.js
  import {foo} from './m1.js';
  console.log(foo);
  setTimeout(() => console.log(foo), 500);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末廉涕,一起剝皮案震驚了整個濱河市泻云,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌狐蜕,老刑警劉巖宠纯,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異层释,居然都是意外死亡婆瓜,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來廉白,“玉大人个初,你說我怎么就攤上這事『秕澹” “怎么了勃黍?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長晕讲。 經(jīng)常有香客問我覆获,道長,這世上最難降的妖魔是什么瓢省? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任弄息,我火速辦了婚禮,結(jié)果婚禮上勤婚,老公的妹妹穿的比我還像新娘摹量。我一直安慰自己,他們只是感情好馒胆,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布缨称。 她就那樣靜靜地躺著,像睡著了一般祝迂。 火紅的嫁衣襯著肌膚如雪睦尽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天型雳,我揣著相機與錄音当凡,去河邊找鬼。 笑死纠俭,一個胖子當(dāng)著我的面吹牛沿量,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冤荆,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼朴则,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了钓简?” 一聲冷哼從身側(cè)響起乌妒,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎涌庭,沒想到半個月后芥被,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體欧宜,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡坐榆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了冗茸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片席镀。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡匹中,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出豪诲,到底是詐尸還是另有隱情顶捷,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布屎篱,位于F島的核電站服赎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏交播。R本人自食惡果不足惜重虑,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望秦士。 院中可真熱鬧缺厉,春花似錦、人聲如沸隧土。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽曹傀。三九已至辐脖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間皆愉,已是汗流浹背揖曾。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留亥啦,地道東北人炭剪。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像翔脱,于是被迫代替她去往敵國和親奴拦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354