分而治之-前端模塊化

Created By JishuBao on 2019-03-20 12:38:22
Recently revised in 2019-03-20 12:38:22javascript

 

  歡迎你們來到技術寶的掘金世界,您的star是我寫文章最大的動力!GitHub地址     css

文章簡介:前端

一、前端模塊化發展簡述java

二、模塊的演變node

三、CommonJs概述jquery

四、AMD概述git

五、CMD概述es6

六、AMD與CMD的區別github

七、嚴格模式web

八、ES6的Module

九、Moudle的加載實現

十、module.exports與exports、export、export default、exports.default區別

1、前端模塊化發展簡述

 歷史上,JavaScript一直沒有模塊(module)體系,沒法將一個大程序拆分紅互相依賴的小文件,再用簡單的方法拼接起來。其餘語言都有這項功能,好比Ruby的require、Python的import,甚至就連css都有@import,可是JavaScript任何這方面的支持都沒有,這對開發大型的、複雜的項目造成了巨大障礙。

 在JavaScript發展初期就是爲了實現簡單的頁面交互邏輯,寥寥數語便可;現在CPU、瀏覽器性能獲得了極大的提高,不少頁面邏輯遷移到了客戶端(表單驗證等),隨着web2.0時代的到來,Ajax技術獲得普遍應用,jQuery等前端庫層出不窮,前端代碼日益膨脹。

 這時候JavaScript做爲嵌入式的腳本語言的定位動搖了,JavaScript卻沒有爲組織代碼提供任何明顯幫助,甚至沒有類的概念,更不用說模塊module了,JavaScript極其簡單的代碼組織規範不足以駕馭如此龐大規模的代碼。

2、模塊的演變

 既然JavaScript不能handle如此大規模的代碼,咱們能夠借鑑一下其它語言是怎麼處理大規模程序設計的,在Java中有一個重要帶概念——package,邏輯上相關的代碼組織到同一個包內,包內是一個相對獨立的王國,不用擔憂命名衝突什麼的,那麼外部若是使用呢?直接import對應的package便可

import java.util.ArrayList;
複製代碼

 遺憾的是JavaScript在設計時定位緣由,沒有提供相似的功能,開發者須要模擬出相似的功能,來隔離、組織複雜的JavaScript代碼,咱們稱爲模塊化。

 一個模塊就是實現特定功能的文件,有了模塊,咱們就能夠更方便地使用別人的代碼,想要什麼功能,就加載什麼模塊。模塊開發須要遵循必定的規範,各行其是就都亂套了。

規範造成的過程是痛苦的,前端的先驅在刀耕火種、茹毛飲血的階段開始,發展到如今初具規模,簡單瞭解一下這段不凡的歷程

1.函數封裝

 咱們在講函數的時候提到,函數一個功能就是實現特定邏輯的一組語句打包,並且JavaScript的做用域就是基於函數的,因此把函數做爲模塊化的第一步是很天然的事情,在一個文件裏面編寫幾個相關函數就是最開始的模塊了。

function fn1(){
    statement
    ......
}

function fn2(){
    statement
    ......
}
複製代碼

 這樣在須要的之後夾在函數所在文件,調用函數就能夠了。

 這種作法的缺點很明顯:污染了全局變量,沒法保證不與其餘模塊發生變量名衝突,並且模塊成員之間沒什麼關係。

2.對象

 爲了解決上面問題,對象的寫法應運而生,能夠把全部的模塊成員封裝在一個對象中

var myModule = {
    var1: 1,

    var2: 2,

    fn1: function(){

    },

    fn2: function(){

    }
}
複製代碼

 這樣咱們在但願調用模塊的時候引用對應文件,而後

myModule.fn2();
複製代碼

 這樣避免了變量污染,只要保證模塊名惟一便可,同時同一模塊內的成員也有了關係

 看似不錯的解決方案,可是也有缺陷,外部能夠隨意修改內部成員

myModel.var1 = 100;
複製代碼

 這樣就會產生意外的安全問題

3.當即執行函數

 能夠經過當即執行函數,來達到隱藏細節的目的。

var myModule = (function(){
    var var1 = 1;
    var var2 = 2;

    function fn1(){

    }

    function fn2(){

    }

    return {
        fn1: fn1,
        fn2: fn2
    };
})();
複製代碼

 這樣在模塊外部沒法修改咱們沒有暴露出來的變量、函數

 上述作法就是咱們模塊化的基礎,目前,通行的JavaScript模塊規範主要有兩種:CommonJSAMD

3、CommonJs概述

 咱們先從CommonJS談起,由於在網頁端沒有模塊化編程只是頁面JavaScript邏輯複雜,但也能夠工做下去,在服務器端卻必定要有模塊,因此雖然JavaScript在web端發展這麼多年,第一個流行的模塊化規範卻由服務器端的JavaScript應用帶來,CommonJS規範是由NodeJS發揚光大,這標誌着JavaScript模塊化編程正式登上舞臺

  • 定義模塊:根據CommonJS規範,一個單獨的文件就是一個模塊每個模塊都是一個單獨的做用域,也就是說,在該模塊內部定義的變量,沒法被其餘模塊讀取,除非定義爲global對象的屬性。
  • 模塊輸出:模塊只有一個出口,module.exports對象,咱們須要把模塊但願輸出的內容放入該對象
  • 加載模塊:加載模塊使用require方法,該方法讀取一個文件並執行,返回文件內部的module.exports對象
//模塊定義 myModel.js

var name = 'Byron';

function printName(){
    console.log(name);
}

function printFullName(firstName){
    console.log(firstName + name);
}

module.exports = {
    printName: printName,
    printFullName: printFullName
}

//加載模塊

var nameModule = require('./myModel.js');

nameModule.printName();
複製代碼

 不一樣的實現對require時的路徑有不一樣要求,通常狀況能夠省略js拓展名,能夠使用相對路徑,也能夠使用絕對路徑,甚至能夠省略路徑直接使用模塊名(前提是該模塊是系統內置模塊)

尷尬的瀏覽器

 仔細看上面的代碼,會發現require是同步的。模塊系統須要同步讀取模塊文件內容,並編譯執行以獲得模塊接口

 這在服務器端實現很簡單,也很天然,然而, 想在瀏覽器端實現問題卻不少。

瀏覽器端,加載JavaScript最佳、最容易的方式是在document中插入script 標籤。但腳本標籤天生異步,傳統CommonJS模塊在瀏覽器環境中沒法正常加載

 解決思路之一是,開發一個服務器端組件,對模塊代碼做靜態分析,將模塊與它的依賴列表一塊兒返回給瀏覽器端。 這很好使,但須要服務器安裝額外的組件,並所以要調整一系列底層架構。

 另外一種解決思路是,用一套標準模板來封裝模塊定義,可是對於模塊應該怎麼定義和怎麼加載,又產生的分歧

4、AMD概述

 AMD 即Asynchronous Module Definition,中文名是異步模塊定義的意思。它是一個在瀏覽器端模塊化開發的規範

 因爲不是JavaScript原生支持使用AMD規範進行頁面開發須要用到對應的庫函數,也就是大名鼎鼎RequireJS,實際上AMD 是 RequireJS 在推廣過程當中對模塊定義的規範化的產出

requireJS主要解決兩個問題

  • 多個js文件可能有依賴關係,被依賴的文件須要早於依賴它的文件加載到瀏覽器。
  • js加載的時候瀏覽器會中止頁面渲染,加載文件越多,頁面失去響應時間越長
// 定義模塊 myModule.js
define(['myModule'], function(){
    var name = 'Byron';
    function printName(){
        console.log(name);
    }

    return {
        printName: printName
    };
});

// 加載模塊
require(['myModule'], function (my){
    my.printName();
});
複製代碼

語法

 requireJS定義了一個函數 define,它是全局變量,用來定義模塊

 define(id?, dependencies?, factory);

  • id:可選參數,用來定義模塊的標識,若是沒有提供該參數,腳本文件名(去掉拓展名)

  • dependencies:是一個當前模塊依賴的模塊名稱數組

  • factory:工廠方法,模塊初始化要執行的函數或對象。若是爲函數,它應該只被執行一次。若是是對象,此對象應該爲模塊的輸出值

 在頁面上使用require函數加載模塊

 require([dependencies], function(){});

 require()函數接受兩個參數

  • 第一個參數是一個數組,表示所依賴的模塊
  • 第二個參數是一個回調函數,當前面指定的模塊都加載成功後,它將被調用。加載的模塊會以參數形式傳入該函數,從而在回調函數內部就能夠使用這些模塊

require()函數在加載依賴的函數的時候是異步加載的,這樣瀏覽器不會失去響應,它指定的回調函數,只有前面的模塊都加載成功後,纔會運行,解決了依賴性的問題。

5、CMD概述

 CMD 即Common Module Definition通用模塊定義,CMD規範是國內發展出來的,就像AMD有個requireJS,CMD有個瀏覽器的實現SeaJSSeaJS要解決的問題和requireJS同樣,只不過在模塊定義方式和模塊加載(能夠說運行、解析)時機上有所不一樣。

語法

Sea.js 推崇一個模塊一個文件,遵循統一的寫法

define

define(id?, deps?, factory)

 由於CMD推崇

  • 一個文件一個模塊,因此常常就用文件名做爲模塊id
  • CMD推崇依賴就近,因此通常不在define的參數中寫依賴,在factory中寫

 factory有三個參數

function(require, exports, module) 複製代碼

require

 require 是 factory 函數的第一個參數

 require(id)

 require 是一個方法,接受 模塊標識 做爲惟一參數,用來獲取其餘模塊提供的接口

exports

 exports 是一個對象,用來向外提供模塊接口

module

 module 是一個對象,上面存儲了與當前模塊相關聯的一些屬性和方法

// 定義模塊 myModule.js
define(function(require, exports, module) {
  var $ = require('jquery.js')
  $('div').addClass('active');
});

// 加載模塊
seajs.use(['myModule.js'], function(my){

});
複製代碼

6、AMD與CMD的區別

 最明顯的區別就是在模塊定義時對依賴的處理不一樣

  • AMD推崇依賴前置,在定義模塊的時候就要聲明其依賴的模塊
  • CMD推崇就近依賴,只有在用到某個模塊的時候再去require

 這種區別各有優劣,只是語法上的差距,並且requireJS和SeaJS都支持對方的寫法。

 最的區別是對依賴模塊的執行時機處理不一樣,注意不是加載的時機或者方式不一樣

 不少人說requireJS是異步加載模塊,SeaJS是同步加載模塊,這麼理解其實是不許確的,其實加載模塊都是異步的,只不過AMD依賴前置,js能夠方便知道依賴模塊是誰,當即加載,而CMD就近依賴,須要使用把模塊變爲字符串解析一遍才知道依賴了那些模塊,這也是不少人詬病CMD的一點,犧牲性能來帶來開發的便利性,實際上解析模塊用的時間短到能夠忽略

 爲何咱們說兩個的區別是依賴模塊執行時機不一樣,爲何不少人認爲ADM是異步的,CMD是同步的(除了名字的緣由。。。)

 一樣都是異步加載模塊,AMD在加載模塊完成後就會執行改模塊,全部模塊都加載執行完後會進入require的回調函數,執行主邏輯,這樣的效果就是依賴模塊的執行順序和書寫順序不必定一致,看網絡速度,哪一個先下載下來,哪一個先執行,可是主邏輯必定在全部依賴加載完成後才執行。

 CMD加載完某個依賴模塊後並不執行,只是下載而已,在全部依賴模塊加載完成後進入主邏輯,遇到require語句的時候才執行對應的模塊,這樣模塊的執行順序和書寫順序是徹底一致的。

 這也是不少人說AMD用戶體驗好,由於沒有延遲,依賴模塊提早執行了,CMD性能好,由於只有用戶須要的時候才執行的緣由。

7、嚴格模式

 ES6 的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict",嚴格模式主要有如下限制。

  • 變量必須聲明後再使用
  • 函數的參數不能有同名屬性,不然報錯
  • 不能使用with語句
  • 不能對只讀屬性賦值,不然報錯
  • 不能使用前綴 0 表示八進制數,不然報錯
  • 不能刪除不可刪除的屬性,不然報錯
  • 不能刪除變量delete prop,會報錯,只能刪除屬性delete global[prop]
  • eval不會在它的外層做用域引入變量
  • eval和arguments不能被從新賦值
  • arguments不會自動反映函數參數的變化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局對象
  • 不能使用fn.caller和fn.arguments獲取函數調用的堆棧
  • 增長了保留字(好比protected、static和interface)

 其中,尤爲須要注意this的限制。ES6模塊之中,頂層的this指向undefined,即不該該在頂層代碼使用this

8、ES6的Module

1.export命令

 模塊功能主要由兩個命令構成:exportimportexport命令用於規定模塊的對外接口,import命令用於輸入其餘模塊提供的功能。

 一個模塊就是一個獨立的文件。該文件內部的全部變量,外部沒法獲取。若是你但願外部可以讀取模塊內部的某個變量,就必須使用export關鍵字輸出該變量

// profile.js
//第一種寫法
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

//第二種寫法
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName,lastName,year}
複製代碼

 上面代碼是profile.js文件,保存了用戶信息。ES6 將其視爲一個模塊,裏面用export命令對外部輸出了三個變量

 它與前一種寫法(直接放置在var語句前)是等價的,可是應該優先考慮使用這種寫法。由於這樣就能夠在腳本尾部,一眼看清楚輸出了哪些變量

 export命令除了輸出變量,還能夠輸出函數或類(class)

export function multiply(x, y) {
  return x * y;
};
複製代碼

 一般狀況下,export輸出的變量就是原本的名字,可是能夠使用as關鍵字重命名

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};
複製代碼

 上面代碼使用as關鍵字,重命名了函數v1和v2的對外接口。重命名後,v2能夠用不一樣的名字輸出兩次

 須要特別注意的是,export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係

// 報錯
export 1;

// 報錯
var m = 1;
export m;
複製代碼

 上面兩種寫法都會報錯,由於沒有提供對外的接口。第一種寫法直接輸出 1,第二種寫法經過變量m,仍是直接輸出 1。1只是一個值,不是接口。正確的寫法是下面這樣。

// 寫法一
export var m = 1;

// 寫法二
var m = 1;
export {m};

// 寫法三
var n = 1;
export {n as m};
複製代碼

 上面三種寫法都是正確的,規定了對外的接口m。其餘腳本能夠經過這個接口,取到值1。它們的實質是,在接口名與模塊內部變量之間,創建了一一對應的關係

 一樣的,functionclass的輸出,也必須遵照這樣的寫法。

// 報錯
function f() {}
export f;

// 正確
export function f() {};

// 正確
function f() {}
export {f};
複製代碼

 另外,export語句輸出的接口,與其對應的值是動態綁定關係,即經過該接口,能夠取到模塊內部實時的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
複製代碼

 上面代碼輸出變量foo,值爲bar,500 毫秒以後變成baz

 最後,export命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。若是處於塊級做用域內,就會報錯,下一節的import命令也是如此。這是由於處於條件代碼塊之中,就無法作靜態優化了,違背了 ES6 模塊的設計初衷。

function foo() {
  export default 'bar' // SyntaxError
}
foo()
複製代碼

 上面代碼中,export語句放在函數之中,結果報錯

2.import命令

 使用export命令定義了模塊的對外接口之後,其餘JS文件就能夠經過import命令加載這個模塊

// main.js
import {firstName, lastName, year} from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}
複製代碼

 上面代碼的import命令,用於加載profile.js文件,並從中輸入變量。import命令接受一對大括號,裏面指定要從其餘模塊導入的變量名。大括號裏面的變量名,必須與被導入模塊(profile.js)對外接口的名稱相同。

 若是想爲輸入的變量從新取一個名字,import命令要使用as關鍵字,將輸入的變量重命名。

import { lastName as surname } from './profile.js';
複製代碼

 import命令輸入的變量都是只讀的,由於它的本質是輸入接口。也就是說,不容許在加載模塊的腳本里面,改寫接口。

import {a} from './xxx.js'

a = {}; // Syntax Error : 'a' is read-only;
複製代碼

 上面代碼中,腳本加載了變量a,對其從新賦值就會報錯,由於a是一個只讀的接口。可是,若是a是一個對象,改寫a的屬性是容許的

import {a} from './xxx.js'

a.foo = 'hello'; // 合法操做
複製代碼

 上面代碼中,a的屬性能夠成功改寫,而且其餘模塊也能夠讀到改寫後的值。不過,這種寫法很難查錯,建議凡是輸入的變量,都看成徹底只讀,輕易不要改變它的屬性。

 import後面的from指定模塊文件的位置,能夠是相對路徑,也能夠是絕對路徑,.js後綴能夠省略。若是隻是模塊名,不帶有路徑,那麼必須有配置文件,告訴 JavaScript 引擎該模塊的位置

import {myMethod} from 'util';
複製代碼

 上面代碼中,util是模塊文件名,因爲不帶有路徑,必須經過配置,告訴引擎怎麼取到這個模塊。

 注意,import命令具備提高效果,會提高到整個模塊的頭部,首先執行

foo();

import { foo } from 'my_module';
複製代碼

 上面的代碼不會報錯,由於import的執行早於foo的調用。這種行爲的本質是,import命令是編譯階段執行的,在代碼運行以前。

 因爲import是靜態執行,因此不能使用表達式和變量,這些只有在運行時才能獲得結果的語法結構

// 報錯
import { 'f' + 'oo' } from 'my_module';

// 報錯
let module = 'my_module';
import { foo } from module;

// 報錯
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}
複製代碼

 上面三種寫法都會報錯,由於它們用到了表達式、變量和if結構。在靜態分析階段,這些語法都是無法獲得值的。

 最後,import語句會執行所加載的模塊,所以能夠有下面的寫法。

import 'lodash';
複製代碼

 上面代碼僅僅執行lodash模塊,可是不輸入任何值。若是屢次重複執行同一句import語句,那麼只會執行一次,而不會執行屢次

import 'lodash';
import 'lodash';
複製代碼

 上面代碼加載了兩次lodash,可是隻會執行一次。

import { foo } from 'my_module';
import { bar } from 'my_module';

// 等同於
import { foo, bar } from 'my_module';
複製代碼

 上面代碼中,雖然foo和bar在兩個語句中加載,可是它們對應的是同一個my_module實例。也就是說,import語句是 Singleton 模式。

 目前階段,經過Babel 轉碼,CommonJS 模塊的require命令和 ES6模塊的import命令,能夠寫在同一個模塊裏面,可是最好不要這樣作。由於import在靜態解析階段執行,因此它是一個模塊之中最先執行的。下面的代碼可能不會獲得預期結果

require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
複製代碼

3.模塊的總體加載

 除了指定加載某個輸出值,還能夠使用總體加載,即用星號(*)指定一個對象,全部輸出值都加載在這個對象上面

 下面是一個circle.js文件,它輸出兩個方法area和circumference。

// circle.js
export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}
複製代碼

 如今,加載這個模塊。

// main.js

import { area, circumference } from './circle';

console.log('圓面積:' + area(4));
console.log('圓周長:' + circumference(14));
複製代碼

 上面寫法是逐一指定要加載的方法,總體加載的寫法以下。

import * as circle from './circle';

console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));
複製代碼

 注意,模塊總體加載所在的那個對象(上例是circle),應該是能夠靜態分析的,因此不容許運行時改變。下面的寫法都是不容許的。

import * as circle from './circle';

// 下面兩行都是不容許的
circle.foo = 'hello';
circle.area = function () {};
複製代碼

4.export default命令

 從前面的例子能夠看出,使用import命令的時候,用戶須要知道所要加載的變量名或函數名,不然沒法加載。可是,用戶確定但願快速上手,未必願意閱讀文檔,去了解模塊有哪些屬性和方法。

 爲了給用戶提供方便,讓他們不用閱讀文檔就能加載模塊,就要用到export default命令,爲模塊指定默認輸出。

// export-default.js
export default function () {
  console.log('foo');
}
複製代碼

 上面代碼是一個模塊文件export-default.js,它的默認輸出是一個函數

 其餘模塊加載該模塊時,import命令能夠爲該匿名函數指定任意名字

// import-default.js
import customName from './export-default';
customName(); // 'foo'
複製代碼

 上面代碼的import命令,能夠用任意名稱指向export-default.js輸出的方法,這時就不須要知道原模塊輸出的函數名。須要注意的是,這時import命令後面,不使用大括號

 export default命令用在非匿名函數前,也是能夠的。

// export-default.js
export default function foo() {
  console.log('foo');
}

// 或者寫成

function foo() {
  console.log('foo');
}

export default foo;
複製代碼

 上面代碼中,foo函數的函數名foo,在模塊外部是無效的。加載的時候,視同匿名函數加載。

 下面比較一下默認輸出和正常輸出。

// 第一組
export default function crc32() { // 輸出
  // ...
}

import crc32 from 'crc32'; // 輸入

// 第二組
export function crc32() { // 輸出
  // ...
};

import {crc32} from 'crc32'; // 輸入
複製代碼

 上面代碼的兩組寫法,第一組是使用export default時,對應的import語句不須要使用大括號;第二組是不使用export default時,對應的import語句須要使用大括號。

 export default命令用於指定模塊的默認輸出。顯然,一個模塊只能有一個默認輸出,所以export default命令只能使用一次。因此,import命令後面纔不用加大括號,由於只可能惟一對應export default命令。

 本質上,export default就是輸出一個叫作default的變量或方法,而後系統容許你爲它取任意名字。因此,下面的寫法是有效的。

// modules.js
function add(x, y) {
  return x * y;
}
export {add as default};
// 等同於
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同於
// import foo from 'modules';
複製代碼

 正是由於export default命令其實只是輸出一個叫作default的變量,因此它後面不能跟變量聲明語句。

// 正確
export var a = 1;

// 正確
var a = 1;
export default a;

// 錯誤
export default var a = 1;
複製代碼

 上面代碼中,export default a的含義是將變量a的值賦給變量default。因此,最後一種寫法會報錯。

 一樣地,由於export default命令的本質是將後面的值,賦給default變量,因此能夠直接將一個值寫在export default以後。

// 正確
export default 42;

// 報錯
export 42;
複製代碼

 上面代碼中,後一句報錯是由於沒有指定對外的接口,而前一句指定對外接口爲default。

 有了export default命令,輸入模塊時就很是直觀了,以輸入 lodash 模塊爲例。

import _ from 'lodash';
複製代碼

 若是想在一條import語句中,同時輸入默認方法和其餘接口,能夠寫成下面這樣。

import _, { each, forEach } from 'lodash';
複製代碼

 對應上面代碼的export語句以下。

export default function (obj) {
  // ···
}

export function each(obj, iterator, context) {
  // ···
}

export { each as forEach };
複製代碼

 上面代碼的最後一行的意思是,暴露出forEach接口,默認指向each接口,即forEach和each指向同一個方法。

 export default也能夠用來輸出類。

// MyClass.js
export default class { ... }

// main.js
import MyClass from 'MyClass';
let o = new MyClass();
複製代碼

5.export 與 import的複合寫法

 若是在一個模塊之中,先輸入後輸出同一個模塊import語句能夠與export語句寫在一塊兒。

export { foo, bar } from 'my_module';

// 能夠簡單理解爲
import { foo, bar } from 'my_module';
export { foo, bar };
複製代碼

 上面代碼中,export和import語句能夠結合在一塊兒,寫成一行。但須要注意的是,寫成一行之後,foo和bar實際上並無被導入當前模塊,只是至關於對外轉發了這兩個接口,致使當前模塊不能直接使用foo和bar

 模塊的接口更名和總體輸出,也能夠採用這種寫法。

// 接口更名
export { foo as myFoo } from 'my_module';

// 總體輸出
export * from 'my_module';
複製代碼

 默認接口的寫法以下。

export { default } from 'foo';
複製代碼

 具名接口改成默認接口的寫法以下。

export { es6 as default } from './someModule';

// 等同於
import { es6 } from './someModule';
export default es6;
複製代碼

 一樣地,默認接口也能夠更名爲具名接口。

export { default as es6 } from './someModule';
複製代碼

 下面三種import語句,沒有對應的複合寫法。

import * as someIdentifier from "someModule";
import someIdentifier from "someModule";
import someIdentifier, { namedIdentifier } from "someModule";
複製代碼

 爲了作到形式的對稱,如今有提案,提出補上這三種複合寫法。

export * as someIdentifier from "someModule";
export someIdentifier from "someModule";
export someIdentifier, { namedIdentifier } from "someModule";
複製代碼

6.模塊的繼承

 模塊之間也能夠繼承。

 假設有一個circleplus模塊,繼承了circle模塊。

// circleplus.js

export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
  return Math.exp(x);
}
複製代碼

 上面代碼中的export *,表示再輸出circle模塊的全部屬性和方法。注意,export *命令會忽略circle模塊的default方法。而後,上面代碼又輸出了自定義的e變量和默認方法

 這時,也能夠將circle的屬性或方法,更名後再輸出。

// circleplus.js
export { area as circleArea } from 'circle';
複製代碼

 上面代碼表示,只輸出circle模塊的area方法,且將其更名爲circleArea

 加載上面模塊的寫法以下。

// main.js

import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));
複製代碼

 上面代碼中的import exp表示,將circleplus模塊的默認方法加載爲exp方法。

7.跨模塊的常量

 本書介紹const命令的時候說過,const聲明的常量只在當前代碼塊有效。若是想設置跨模塊的常量(即跨多個文件),或者說一個值要被多個模塊共享,能夠採用下面的寫法。

// constants.js 模塊
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模塊
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模塊
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
複製代碼

 若是要使用的常量很是多,能夠建一個專門的constants目錄,將各類常量寫在不一樣的文件裏面,保存在該目錄下。

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
複製代碼

 而後,將這些文件輸出的常量,合併在index.js裏面。

// constants/index.js
export {db} from './db';
export {users} from './users';
複製代碼

 使用的時候,直接加載index.js就能夠了。

// script.js
import {db, users} from './constants/index';
複製代碼

8.import()

 前面介紹過,import命令會被 JavaScript 引擎靜態分析,先於模塊內的其餘語句執行(import命令叫作「鏈接」 binding 其實更合適)。因此,下面的代碼會報錯。

// 報錯
if (x === 2) {
  import MyModual from './myModual';
}
複製代碼

 上面代碼中,引擎處理import語句是在編譯時,這時不會去分析或執行if語句,因此import語句放在if代碼塊之中毫無心義,所以會報句法錯誤,而不是執行時錯誤。也就是說,import和export命令只能在模塊的頂層,不能在代碼塊之中(好比,在if代碼塊之中,或在函數之中)

 這樣的設計,當然有利於編譯器提升效率,但也致使沒法在運行時加載模塊。在語法上,條件加載就不可能實現。若是import命令要取代 Node 的require方法,這就造成了一個障礙。由於require是運行時加載模塊,import命令沒法取代require的動態加載功能。

const path = './' + fileName;
const myModual = require(path);
複製代碼

 上面的語句就是動態加載,require到底加載哪個模塊,只有運行時才知道。import命令作不到這一點。

 所以,有一個提案,建議引入import()函數,完成動態加載

import(specifier)
複製代碼

 上面代碼中,import函數的參數specifier,指定所要加載的模塊的位置。import命令可以接受什麼參數,import()函數就能接受什麼參數,二者區別主要是後者爲動態加載

const main = document.querySelector('main');

import(`./section-modules/${someVariable}.js`)
  .then(module => {
    module.loadPageInto(main);
  })
  .catch(err => {
    main.textContent = err.message;
 });
複製代碼

import()函數能夠用在任何地方,不只僅是模塊,非模塊的腳本也能夠使用。它是運行時執行,也就是說,何時運行到這一句,就會加載指定的模塊。另外,import()函數與所加載的模塊沒有靜態鏈接關係,這點也是與import語句不相同。import()相似於 Node 的require方法,區別主要是前者是異步加載,後者是同步加載

適用場景

 下面是import()的一些適用場合。

  • 按需加載

import()能夠在須要的時候,再加載某個模塊

button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then(dialogBox => {
    dialogBox.open();
  })
  .catch(error => {
    /* Error handling */
  })
});
複製代碼

 上面代碼中,import()方法放在click事件的監聽函數之中,只有用戶點擊了按鈕,纔會加載這個模塊

  • 條件加載

import()能夠放在if代碼塊,根據不一樣的狀況,加載不一樣的模塊

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}
複製代碼

 上面代碼中,若是知足條件,就加載模塊 A,不然加載模塊 B。

  • 動態的模塊路徑

import()容許模塊路徑動態生成

import(f())
.then(...);
複製代碼

 上面代碼中,根據函數f的返回結果,加載不一樣的模塊。

注意點

  • import()加載模塊成功之後,這個模塊會做爲一個對象,看成then方法的參數。所以,能夠使用對象解構賦值的語法,獲取輸出接口。
import('./myModule.js')
.then(({export1, export2}) => {
  // ...·
});
複製代碼

 上面代碼中,export1和export2都是myModule.js的輸出接口,能夠解構得到。

 若是模塊有default輸出接口,能夠用參數直接得到。

import('./myModule.js')
.then(myModule => {
  console.log(myModule.default);
});
複製代碼

 上面的代碼也能夠使用具名輸入的形式。

import('./myModule.js')
.then(({default: theDefault}) => {
  console.log(theDefault);
});
複製代碼

 若是想同時加載多個模塊,能夠採用下面的寫法。

Promise.all([
  import('./module1.js'),
  import('./module2.js'),
  import('./module3.js'),
])
.then(([module1, module2, module3]) => {
   ···
});
複製代碼

 import()也能夠用在 async 函數之中。

async function main() {
  const myModule = await import('./myModule.js');
  const {export1, export2} = await import('./myModule.js');
  const [module1, module2, module3] =
    await Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ]);
}
main();
複製代碼

9、Module的加載實現

1.瀏覽器加載

1.傳統方法

HTML 網頁中,瀏覽器經過<script>標籤加載 JavaScript 腳本。

<!-- 頁面內嵌的腳本 -->
<script type="application/javascript"> // module code </script>

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

上面代碼中,因爲瀏覽器腳本的默認語言是 JavaScript,所以type="application/javascript"能夠省略。

默認狀況下,瀏覽器是同步加載 JavaScript 腳本,即渲染引擎遇到<script>標籤就會停下來,等到執行完腳本,再繼續向下渲染。若是是外部腳本,還必須加入腳本下載的時間

若是腳本體積很大,下載和執行的時間就會很長,所以形成瀏覽器堵塞,用戶會感受到瀏覽器「卡死」了,沒有任何響應。這顯然是很很差的體驗,因此瀏覽器容許腳本異步加載,下面就是兩種異步加載的語法。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
複製代碼

上面代碼中,<script>標籤打開defer或async屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行後面的命令。

defer與async的區別是:defer要等到整個頁面在內存中正常渲染結束(DOM 結構徹底生成,以及其餘腳本執行完成),纔會執行;async一旦下載完,渲染引擎就會中斷渲染,執行這個腳本之後,再繼續渲染。一句話,defer是「渲染完再執行」,async是「下載完就執行」。另外,若是有多個defer腳本,會按照它們在頁面出現的順序加載,而多個async腳本是不能保證加載順序的。

2.加載規則

瀏覽器加載 ES6 模塊,也使用<script>標籤,可是要加入type="module"屬性。

<script type="module" src="./foo.js"></script>
複製代碼

上面代碼在網頁中插入一個模塊foo.js,因爲type屬性設爲module,因此瀏覽器知道這是一個 ES6 模塊

瀏覽器對於帶有type="module"的<script>,都是異步加載,不會形成堵塞瀏覽器,即等到整個頁面渲染完,再執行模塊腳本,等同於打開了<script>標籤的defer屬性

<script type="module" src="./foo.js"></script>
<!-- 等同於 -->
<script type="module" src="./foo.js" defer></script>
複製代碼

若是網頁有多個<script type="module">,它們會按照在頁面出現的順序依次執行

<script>標籤的async屬性也能夠打開,這時只要加載完成,渲染引擎就會中斷渲染當即執行。執行完成後,再恢復渲染

<script type="module" src="./foo.js" async></script>
複製代碼

一旦使用了async屬性,<script type="module">就不會按照在頁面出現的順序執行,而是只要該模塊加載完成,就執行該模塊

ES6 模塊也容許內嵌在網頁中,語法行爲與加載外部腳本徹底一致。

<script type="module">
  import utils from "./utils.js";

  // other code
</script>
複製代碼

對於外部的模塊腳本(上例是foo.js),有幾點須要注意。

  • 代碼是在模塊做用域之中運行,而不是在全局做用域運行。模塊內部的頂層變量,外部不可見。
  • 模塊腳本自動採用嚴格模式,無論有沒有聲明use strict。
  • 模塊之中,能夠使用import命令加載其餘模塊(.js後綴不可省略,須要提供絕對 URL 或相對 URL),也能夠使用export命令輸出對外接口。
  • 模塊之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模塊頂層使用this關鍵字,是無心義的。
  • 同一個模塊若是加載屢次,將只執行一次。

下面是一個示例模塊。

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這個語法點,能夠偵測當前代碼是否在 ES6 模塊之中

const isNotModuleScript = this !== undefined;//判斷是否在ES6模塊中
複製代碼

2.ES6模塊與CommonJs模塊加載的差別

  • 1.CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
  • 2.CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

第二個差別是由於 CommonJS 加載的是一個對象(即module.exports屬性),該對象只有在腳本運行完纔會生成。而 ES6 模塊不是對象,它的對外接口只是一種靜態定義,在代碼靜態解析階段就會生成。

下面重點解釋第一個差別。

CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。請看下面這個模塊文件lib.js的例子。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
複製代碼

上面代碼輸出內部變量counter和改寫這個變量的內部方法incCounter。而後,在main.js裏面加載這個模塊。

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3
複製代碼

上面代碼說明,lib.js模塊加載之後,它的內部變化就影響不到輸出的mod.counter了。這是由於mod.counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能獲得內部變更後的值。

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

上面代碼中,輸出的counter屬性其實是一個取值器函數。如今再執行main.js,就能夠正確讀取內部變量counter的變更了。

$ node main.js
3
4
複製代碼

ES6 模塊的運行機制與 CommonJS 不同。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令import,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。換句話說,ES6 的import有點像 Unix 系統的「符號鏈接」,原始值變了,import加載的值也會跟着變。所以,ES6 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊

// 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
複製代碼

上面代碼說明,ES6 模塊輸入的變量counter是活的,徹底反應其所在模塊lib.js內部的變化。

// 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);
複製代碼

上面代碼中,m1.js的變量foo,在剛加載時等於bar,過了 500 毫秒,又變爲等於baz。讓咱們看看,m2.js可否正確讀取這個變化。

$ babel-node m2.js

bar
baz
複製代碼

上面代碼代表,ES6 模塊不會緩存運行結果,而是動態地去被加載的模塊取值,而且變量老是綁定其所在的模塊。

因爲 ES6 輸入的模塊變量,只是一個「符號鏈接」,因此這個變量是隻讀的,對它進行從新賦值會報錯

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError
複製代碼

上面代碼中,main.js從lib.js輸入變量obj,能夠對obj添加屬性,可是從新賦值就會報錯。由於變量obj指向的地址是隻讀的,不能從新賦值,這就比如main.js創造了一個名爲obj的const變量。

最後,export經過接口,輸出的是同一個值。不一樣的腳本加載這個接口,獲得的都是一樣的實例。

// mod.js
function C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

export let c = new C();
複製代碼

上面的腳本mod.js,輸出的是一個C的實例。不一樣的腳本加載這個模塊,獲得的都是同一個實例。

// x.js
import {c} from './mod';
c.add();

// y.js
import {c} from './mod';
c.show();

// main.js
import './x';
import './y';
複製代碼

如今執行main.js,輸出的是1。

$ babel-node main.js
1
複製代碼

這就證實了x.js和y.js加載的都是C的同一個實例。

3.Node加載

1.概述

Node 對 ES6 模塊的處理比較麻煩,由於它有本身的 CommonJS 模塊格式,與 ES6 模塊格式是不兼容的。目前的解決方案是,將二者分開,ES6 模塊和 CommonJS 採用各自的加載方案

Node 要求 ES6 模塊採用.mjs後綴文件名。也就是說,只要腳本文件裏面使用import或者export命令,那麼就必須採用.mjs後綴名。require命令不能加載.mjs文件,會報錯,只有import命令才能夠加載.mjs文件。反過來,.mjs文件裏面也不能使用require命令,必須使用import

目前,這項功能還在試驗階段。安裝 Node v8.5.0 或以上版本,要用--experimental-modules參數才能打開該功能

$ node --experimental-modules my-app.mjs
複製代碼

爲了與瀏覽器的import加載規則相同,Node 的.mjs文件支持 URL 路徑

import './foo?query=1'; // 加載 ./foo 傳入參數 ?query=1
複製代碼

上面代碼中,腳本路徑帶有參數?query=1,Node 會按 URL 規則解讀。同一個腳本只要參數不一樣,就會被加載屢次,而且保存成不一樣的緩存。因爲這個緣由,只要文件名中含有:、%、#、?等特殊字符,最好對這些字符進行轉義。

目前,Node 的import命令只支持加載本地模塊(file:協議),不支持加載遠程模塊。

若是模塊名不含路徑,那麼import命令會去node_modules目錄尋找這個模塊。

import 'baz';
import 'abc/123';
複製代碼

若是模塊名包含路徑,那麼import命令會按照路徑去尋找這個名字的腳本文件

import 'file:///etc/config/app.json';
import './foo';
import './foo?search';
import '../bar';
import '/baz';
複製代碼

若是腳本文件省略了後綴名,好比import './foo',Node 會依次嘗試四個後綴名:./foo.mjs、./foo.js、./foo.json、./foo.node。若是這些腳本文件都不存在,Node 就會去加載./foo/package.json的main字段指定的腳本。若是./foo/package.json不存在或者沒有main字段,那麼就會依次加載./foo/index.mjs、./foo/index.js、./foo/index.json、./foo/index.node。若是以上四個文件仍是都不存在,就會拋出錯誤

最後,Node 的import命令是異步加載,這一點與瀏覽器的處理方法相同。

2.內部變量

ES6 模塊應該是通用的,同一個模塊不用修改,就能夠用在瀏覽器環境和服務器環境。爲了達到這個目標,Node 規定 ES6 模塊之中不能使用 CommonJS 模塊的特有的一些內部變量

首先,就是this關鍵字。ES6 模塊之中,頂層的this指向undefined;CommonJS 模塊的頂層this指向當前模塊,這是二者的一個重大差別

其次,如下這些頂層變量在 ES6 模塊之中都是不存在的。

arguments
require
module
exports
__filename
__dirname
複製代碼

若是你必定要使用這些變量,有一個變通方法,就是寫一個 CommonJS 模塊輸出這些變量,而後再用 ES6 模塊加載這個 CommonJS 模塊。可是這樣一來,該 ES6 模塊就不能直接用於瀏覽器環境了,因此不推薦這樣作。

// expose.js
module.exports = {__dirname};

// use.mjs
import expose from './expose.js';
const {__dirname} = expose;
複製代碼

上面代碼中,expose.js是一個 CommonJS 模塊,輸出變量__dirname,該變量在 ES6 模塊之中不存在。ES6 模塊加載expose.js,就能夠獲得__dirname。

3.ES6模塊加載CommonJs模塊

CommonJS 模塊的輸出都定義在module.exports這個屬性上面。Node 的import命令加載 CommonJS模塊,Node 會自動將module.exports屬性,看成模塊的默認輸出,即等同於export default xxx

// a.js
module.exports = {
  foo: 'hello',
  bar: 'world'
};

// 等同於
export default {
  foo: 'hello',
  bar: 'world'
};
複製代碼

import命令加載上面的模塊,module.exports會被視爲默認輸出,即import命令實際上輸入的是這樣一個對象{ default: module.exports }

因此,一共有三種寫法,能夠拿到 CommonJS 模塊的module.exports

// 寫法一
import baz from './a';
// baz = {foo: 'hello', bar: 'world'};

// 寫法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};

// 寫法三
import * as baz from './a';
// baz = {
// get default() {return module.exports;},
// get foo() {return this.default.foo}.bind(baz),
// get bar() {return this.default.bar}.bind(baz)
// }
複製代碼

上面代碼的第三種寫法,能夠經過baz.default拿到module.exports。foo屬性和bar屬性就是能夠經過這種方法拿到了module.exports

// b.js
module.exports = null;

// es.js
import foo from './b';
// foo = null;

import * as bar from './b';
// bar = { default:null };
複製代碼

上面代碼中,es.js採用第二種寫法時,要經過bar.default這樣的寫法,才能拿到module.exports

// c.js
module.exports = function two() {
  return 2;
};

// es.js
import foo from './c';
foo(); // 2

import * as bar from './c';
bar.default(); // 2
bar(); // throws, bar is not a function
複製代碼

上面代碼中,bar自己是一個對象,不能看成函數調用,只能經過bar.default調用

CommonJS 模塊的輸出緩存機制,在 ES6 加載方式下依然有效

// foo.js
module.exports = 123;
setTimeout(_ => module.exports = null);
複製代碼

上面代碼中,對於加載foo.js的腳本,module.exports將一直是123,而不會變成null

因爲 ES6 模塊是編譯時肯定輸出接口,CommonJS 模塊是運行時肯定輸出接口,因此採用import命令加載 CommonJS 模塊時,不容許採用下面的寫法。

// 不正確
import { readFile } from 'fs';
複製代碼

上面的寫法不正確,由於fs是 CommonJS 格式,只有在運行時才能肯定readFile接口,而import命令要求編譯時就肯定這個接口。解決方法就是改成總體輸入。

// 正確的寫法一
import * as express from 'express';
const app = express.default();

// 正確的寫法二
import express from 'express';
const app = express();
複製代碼

4.CommonJs模塊加載ES6模塊

CommonJS 模塊加載 ES6 模塊,不能使用require命令,而要使用import()函數。ES6 模塊的全部輸出接口,會成爲輸入對象的屬性

// es.mjs
let foo = { bar: 'my-default' };
export default foo;

// cjs.js
const es_namespace = await import('./es.mjs');
// es_namespace = {
// get default() {
// ...
// }
// }
console.log(es_namespace.default);
// { bar:'my-default' }
複製代碼

上面代碼中,default接口變成了es_namespace.default屬性。

// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};

// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
// get foo() {return foo;}
// get bar() {return foo;}
// get f() {return f;}
// get c() {return c;}
// }
複製代碼

4.循環加載

「循環加載」(circular dependency)指的是,a腳本的執行依賴b腳本,而b腳本的執行又依賴a腳本

// a.js
var b = require('b');

// b.js
var a = require('a');
複製代碼

一般,「循環加載」表示存在強耦合,若是處理很差,還可能致使遞歸加載,使得程序沒法執行,所以應該避免出現

可是實際上,這是很難避免的,尤爲是依賴關係複雜的大項目,很容易出現a依賴b,b依賴c,c又依賴a這樣的狀況。這意味着,模塊加載機制必須考慮「循環加載」的狀況

對於 JavaScript 語言來講,目前最多見的兩種模塊格式CommonJS 和 ES6,處理「循環加載」的方法是不同的,返回的結果也不同

1.CommonJs的循環加載

CommonJs 模塊的重要特性是加載時執行,即腳本代碼在require的時候,就會所有執行。一旦出現某個模塊被"循環加載",就只輸出已經執行的部分,還未執行的部分不會輸出。

exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');
複製代碼

上面代碼之中,a.js腳本先輸出一個done變量,而後加載另外一個腳本文件b.js。注意,此時a.js代碼就停在這裏,等待b.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 執行完畢');
複製代碼

上面代碼之中,b.js執行到第二行,就會去加載a.js,這時,就發生了「循環加載」。系統會去a.js模塊對應對象的exports屬性取值,但是由於a.js尚未執行完,從exports屬性只能取回已經執行的部分,而不是最後的值

a.js已經執行的部分,只有一行

exports.done = false;
複製代碼

所以,對於b.js來講,它從a.js只輸入一個變量done,值爲false

而後,b.js接着往下執行,等到所有執行完畢,再把執行權交還給a.js。因而,a.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);
複製代碼

執行main.js,運行結果以下。

$ node main.js

在 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的執行結果,即它的第四行

exports.done = true;
複製代碼

總之,CommonJS 輸入的是被輸出值的拷貝,不是引用

另外,因爲 CommonJS 模塊遇到循環加載時,返回的是當前已經執行的部分的值,而不是代碼所有執行後的值,二者可能會有差別。因此,輸入變量的時候,必須很是當心

var a = require('a'); // 安全的寫法
var foo = require('a').foo; // 危險的寫法

exports.good = function (arg) {
  return a.foo('good', arg); // 使用的是 a.foo 的最新值
};

exports.bad = function (arg) {
  return foo('bad', arg); // 使用的是一個部分加載時的值
};
複製代碼

上面代碼中,若是發生循環加載,require('a').foo的值極可能後面會被改寫,改用require('a')會更保險一點

2.ES6模塊的循環加載

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';
複製代碼

上面代碼中,a.mjs加載b.mjs,b.mjs又加載a.mjs,構成循環加載。執行a.mjs,結果以下。

$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined
複製代碼

上面代碼中,執行a.mjs之後會報錯,foo變量未定義,這是爲何?

讓咱們一行行來看,ES6 循環加載是怎麼處理的。首先,執行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};
複製代碼

這時再執行a.mjs就能夠獲得預期結果。

$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar
複製代碼

這是由於函數具備提高做用,在執行import {bar} from './b'時,函數foo就已經有定義了,因此b.mjs加載的時候不會報錯。這也意味着,若是把函數foo改寫成函數表達式,也會報錯。

// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
export {foo};
複製代碼

上面代碼的第四行,改爲了函數表達式,就不具備提高做用,執行就會報錯

// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
  counter++;
  return n === 0 || odd(n - 1);
}

// odd.js
import { even } from './even';
export function odd(n) {
  return n !== 0 && even(n - 1);
}
複製代碼

上面代碼中,even.js裏面的函數even有一個參數n,只要不等於 0,就會減去 1,傳入加載的odd()。odd.js也會作相似操做。

$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17
複製代碼

上面代碼中,參數n從 10 變爲 0 的過程當中,even()一共會執行 6 次,因此變量counter等於 6。第二次調用even()時,參數n從 20 變爲 0,even()一共會執行 11 次,加上前面的 6 次,因此變量counter等於 17。

// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function (n) {
  counter++;
  return n == 0 || odd(n - 1);
}

// odd.js
var even = require('./even').even;
module.exports = function (n) {
  return n != 0 && even(n - 1);
}
複製代碼

上面代碼中,even.js加載odd.js,而odd.js又去加載even.js,造成「循環加載」。這時,執行引擎就會輸出even.js已經執行的部分(不存在任何結果),因此在odd.js之中,變量even等於undefined,等到後面調用even(n - 1)就會報錯。

$ node
> var m = require('./even');
> m.even(10)
TypeError: even is not a function 複製代碼

5.ES6模塊的轉碼

瀏覽器目前還不支持 ES6 模塊,爲了如今就能使用,能夠將其轉爲 ES5 的寫法。除了 Babel 能夠用來轉碼以外,還有如下兩個方法,也能夠用來轉碼。

1.ES6 module transpiler

ES6 module transpiler是 square 公司開源的一個轉碼器,能夠將 ES6 模塊轉爲 CommonJS 模塊或 AMD 模塊的寫法,從而在瀏覽器中使用。

首先,安裝這個轉碼器。

$ npm install -g es6-module-transpiler
複製代碼

而後,使用compile-modules convert命令,將 ES6 模塊文件轉碼。

$ compile-modules convert file1.js file2.js
複製代碼

-o參數能夠指定轉碼後的文件名。

$ compile-modules convert -o out.js file1.js
複製代碼

2.SystemJs

另外一種解決方法是使用 SystemJS。它是一個墊片庫(polyfill),能夠在瀏覽器內加載 ES6 模塊、AMD 模塊和 CommonJS 模塊,將其轉爲 ES5 格式。它在後臺調用的是 Google 的 Traceur 轉碼器。

使用時,先在網頁內載入system.js文件。

<script src="system.js"></script>
複製代碼

而後,使用System.import方法加載模塊文件。

<script>
  System.import('./app.js');
</script>
複製代碼

上面代碼中的./app,指的是當前目錄下的 app.js 文件。它能夠是 ES6 模塊文件,System.import會自動將其轉碼。

須要注意的是,System.import使用異步加載,返回一個 Promise 對象,能夠針對這個對象編程。下面是一個模塊文件。

// app/es6-file.js:

export class q {
  constructor() {
    this.es6 = 'hello';
  }
}
複製代碼

而後,在網頁內加載這個模塊文件。

<script>

System.import('app/es6-file').then(function(m) {
  console.log(new m.q().es6); // hello
});

</script>
複製代碼

上面代碼中,System.import方法返回的是一個 Promise 對象,因此能夠用then方法指定回調函數。

10、module.exports與exports、export、export default、exports.default區別

首先咱們要明白一個前提,CommonJS模塊規範和ES6模塊規範徹底是兩種不一樣的概念。

1.CommonJs模塊規範

Node應用由模塊組成,採用CommonJS模塊規範。

根據這個規範,每一個文件就是一個模塊,有本身的做用域。在一個文件裏面定義的變量、函數、類,都是私有的,對其餘文件不可見。

CommonJS規範規定,每一個模塊內部,module變量表明當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,實際上是加載該模塊的module.exports屬性。

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
複製代碼

上面代碼經過module.exports輸出變量x和函數addX

require方法用於加載模塊

var example = require('./example.js');

console.log(example.x); // 5
console.log(example.addX(1)); // 6
複製代碼

2.ES6模塊規範

不一樣於CommonJS,ES6使用 export 和 import 來導出、導入模塊。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};
複製代碼

須要特別注意的是,export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係

// 寫法一
export var m = 1;

// 寫法二
var m = 1;
export {m};

// 寫法三
var n = 1;
export {n as m};
複製代碼

3.exports與module.exports

爲了方便,Node爲每一個模塊提供一個exports變量,指向module.exports。這等同在每一個模塊頭部,有一行這樣的命令。

var exports = module.exports;
複製代碼

因而咱們能夠直接在 exports 對象上添加方法,表示對外輸出的接口,如同在module.exports上添加同樣。注意,不能直接將exports變量指向一個值,由於這樣等於切斷了exports與module.exports的聯繫。

4.export default 與 exports.default 命令

使用export default命令,爲模塊指定默認輸出

require 引入的是 module.exports

import 引入的是module.exports.default

// export-default.js
export default function () {
  console.log('foo');
}
複製代碼

若是你以爲個人文章還不錯的話,能夠給個star哦~,GitHub地址

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息