JavaScript的模塊

 

介紹

模塊一般是指編程語言所提供的代碼組織機制,利用此機制可將程序拆解爲獨立且通用的代碼單元。所謂模塊化主要是解決代碼分割、做用域隔離、模塊之間的依賴管理以及發佈到生產環境時的自動化打包與處理等多個方面。前端

模塊的優勢

  1. 可維護性。 由於模塊是獨立的,一個設計良好的模塊會讓外面的代碼對本身的依賴越少越好,這樣本身就能夠獨立去更新和改進。
  2. 命名空間。 在 JavaScript 裏面,若是一個變量在最頂級的函數以外聲明,它就直接變成全局可用。所以,經常不當心出現命名衝突的狀況。使用模塊化開發來封裝變量,能夠避免污染全局環境。
  3. 重用代碼。 咱們有時候會喜歡從以前寫過的項目中拷貝代碼到新的項目,這沒有問題,可是更好的方法是,經過模塊引用的方式,來避免重複的代碼庫。

CommonJS

CommonJS 最開始是 Mozilla 的工程師於 2009 年開始的一個項目,它的目的是讓瀏覽器以外的 JavaScript (好比服務器端或者桌面端)可以經過模塊化的方式來開發和協做。node

在 CommonJS 的規範中,每一個 JavaScript 文件就是一個獨立的模塊上下文(module context),在這個上下文中默認建立的屬性都是私有的。也就是說,在一個文件定義的變量(還包括函數和類),都是私有的,對其餘文件是不可見的。web

須要注意的是,CommonJS 規範的主要適用場景是服務器端編程,因此採用同步加載模塊的策略。若是咱們依賴3個模塊,代碼會一個一個依次加載它們。express

該模塊實現方案主要包含 require 與 module 這兩個關鍵字,其容許某個模塊對外暴露部分接口而且由其餘模塊導入使用。npm

//sayModule.js
function SayModule () {
    this.hello = function () {
        console.log('hello');
    };
 
    this.goodbye = function () {
        console.log('goodbye');
    };
}
 
module.exports = SayModule;
 
//main.js 引入sayModule.js
var Say = require('./sayModule.js');
var sayer = new Say();
sayer.hello(); //hello

做爲一個服務器端的解決方案,CommonJS 須要一個兼容的腳本加載器做爲前提條件。該腳本加載器必須支持名爲 require 和 module.exports 的函數,它們將模塊相互導入導出。編程

Node.js

Node 從 CommonJS 的一些創意中,創造出本身的模塊化實現。因爲Node 在服務端的流行,Node 的模塊形式被(不正確地)稱爲 CommonJS。json

Node.js模塊能夠分爲兩大類,一類是核心模塊,另外一類是文件模塊。
核心模塊 就是Node.js標準的API中提供的模塊,如fs、http、net等,這些都是由Node.js官方提供的模塊,編譯成了二進制代碼,能夠直接經過require獲取核心模塊,例如require(‘fs’),核心模塊擁有最高的加載優先級,若是有模塊與核心模塊命名衝突,Node.js老是會加載核心模塊。
文件模塊 是存儲爲單獨的文件(或文件夾)的模塊,多是JavaScript代碼、JSON或編譯好的C/C++代碼。在不顯式指定文件模塊擴展名的時候,Node.js會分別試圖加上.js、.json、.node(編譯好的C/C++代碼)。數組

加載方式瀏覽器

  • 按路徑加載模塊

若是require參數一」/」開頭,那麼就以絕對路徑的方式查找模塊名稱,若是參數一」./」、」../」開頭,那麼則是以相對路徑的方式來查找模塊。緩存

  • 經過查找node_modules目錄加載模塊

若是require參數不以」/」、」./」、」../」開頭,而該模塊又不是核心模塊,那麼就要經過查找node_modules加載模塊了。咱們使用的npm獲取的包一般就是以這種方式加載的。

加載緩存

Node.js模塊不會被重複加載,這是由於Node.js經過文件名緩存全部加載過的文件模塊,因此之後再訪問到時就不會從新加載了。
注意: Node.js是根據實際文件名緩存的,而不是require()提供的參數緩存的,也就是說即便你分別經過require(‘express’)和require(‘./node_modules/express’)加載兩次,也不會重複加載,由於儘管兩次參數不一樣,解析到的文件倒是同一個。

Node.js 中的模塊在加載以後是以單例化運行,而且遵循值傳遞原則:若是是一個對象,就至關於這個對象的引用。

模塊載入過程

加載文件模塊的工做,主要由原生模塊module來實現和完成,該原生模塊在啓動時已經被加載,進程直接調用到runMain靜態方法。

例如運行: node app.js
 
Module.runMain = function () {
    // Load the main module--the command line argument.
    Module._load(process.argv[1], null, true);
};
 
//_load靜態方法在分析文件名以後執行
var module = new Module(id, parent);
 
//並根據文件路徑緩存當前模塊對象,該模塊實例對象則根據文件名加載。
module.load(filename);

具體說一下上文提到了文件模塊的三類模塊,這三類文件模塊之後綴來區分,Node.js會根據後綴名來決定加載方法,具體的加載方法在下文require.extensions中會介紹。

  • .js 經過fs模塊同步讀取js文件並編譯執行。
  • .node 經過C/C++進行編寫的Addon。經過dlopen方法進行加載。
  • .json 讀取文件,調用JSON.parse解析加載。

接下來詳細描述js後綴的編譯過程。Node.js在編譯js文件的過程當中實際完成的步驟有對js文件內容進行頭尾包裝。以app.js爲例,包裝以後的app.js將會變成如下形式:

//circle.js
var PI = Math.PI;
exports.area = function (r) {
    return PI * r * r;
};
exports.circumference = function (r) {
    return 2 * PI * r;
};
 
//app.js
var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is ' + circle.area(4));
 
//app包裝後
(function (exports, require, module, __filename, __dirname) {
    var circle = require('./circle.js');
    console.log('The area of a circle of radius 4 is ' + circle.area(4));
});
 
//這段代碼會經過vm原生模塊的runInThisContext方法執行(相似eval,只是具備明確上下文,不污染全局),返回爲一個具體的function對象。最後傳入module對象的exports,require方法,module,文件名,目錄名做爲實參並執行。

這就是爲何require並無定義在app.js 文件中,可是這個方法卻存在的緣由。從Node.js的API文檔中能夠看到還有__filename__dirnamemoduleexports幾個沒有定義可是卻存在的變量。其中__filename__dirname在查找文件路徑的過程當中分析獲得後傳入的。module變量是這個模塊對象自身,exports是在module的構造函數中初始化的一個空對象({},而不是null)。
在這個主文件中,能夠經過require方法去引入其他的模塊。而其實這個require方法實際調用的就是module._load方法。
load方法在載入、編譯、緩存了module後,返回module的exports對象。這就是circle.js文件中只有定義在exports對象上的方法才能被外部調用的緣由。

以上所描述的模塊載入機制均定義在lib/module.js中。

require 函數

require 引入的對象主要是函數。當 Node 調用 require() 函數,而且傳遞一個文件路徑給它的時候,Node 會經歷以下幾個步驟:

  • Resolving:找到文件的絕對路徑;
  • Loading:判斷文件內容類型;
  • Wrapping:打包,給這個文件賦予一個私有做用範圍。這是使 require 和 module 模塊在本地引用的一種方法;
  • Evaluating:VM 對加載的代碼進行處理的地方;
  • Caching:當再次須要用這個文件的時候,不須要重複一遍上面步驟。

require.extensions 來查看對三種文件的支持狀況

bV0sHs

能夠清晰地看到 Node 對每種擴展名所使用的函數及其操做:對 .js 文件使用 module._compile;對 .json 文件使用 JSON.parse;對 .node 文件使用 process.dlopen。

文件查找策略

  • 從文件模塊緩存中加載

儘管原生模塊與文件模塊的優先級不一樣,可是優先級最高的是從文件模塊的緩存中加載已經存在的模塊。

  • 從原生模塊加載

原生模塊的優先級僅次於文件模塊緩存的優先級。require方法在解析文件名以後,優先檢查模塊是否在原生模塊列表中。以http模塊爲例,儘管在目錄下存在一個httphttp.jshttp.nodehttp.json文件,require(「http」)都不會從這些文件中加載,而是從原生模塊中加載。
原生模塊也有一個緩存區,一樣也是優先從緩存區加載。若是緩存區沒有被加載過,則調用原生模塊的加載方式進行加載和執行。

  • 從文件加載

當文件模塊緩存中不存在,並且不是原生模塊的時候,Node.js會解析require方法傳入的參數,並從文件系統中加載實際的文件,加載過程當中的包裝和編譯細節在前面說過是調用load方法。

當 Node 遇到 require(X) 時,按下面的順序處理。
 
(1)若是 X 是內置模塊(好比 require('http')) 
  a. 返回該模塊。 
  b. 再也不繼續執行。
 
(2)若是 X 以 "./" 或者 "/" 或者 "../" 開頭 
  a. 根據 X 所在的父模塊,肯定 X 的絕對路徑。 
  b. 將 X 當成文件,依次查找下面文件,只要其中有一個存在,就返回該文件,再也不繼續執行。
        X
        X.js
        X.json
        X.node
 
  c. 將 X 當成目錄,依次查找下面文件,只要其中有一個存在,就返回該文件,再也不繼續執行。
        X/package.json(main字段)
        X/index.js
        X/index.json
        X/index.node
 
(3)若是 X 不帶路徑 
  a. 根據 X 所在的父模塊,肯定 X 可能的安裝目錄。 
  b. 依次在每一個目錄中,將 X 當成文件名或目錄名加載。
 
(4) 拋出 "not found"

bV0sHG
··

模塊循環依賴

 

//建立兩個文件,module1.js 和 module2.js,而且讓它們相互引用
    // module1.js
    exports.a = 1;
    require('./module2');
    exports.b = 2;
    exports.c = 3;
    
    // module2.js
    const Module1 = require('./module1');
    console.log('Module1 is partially loaded here', Module1);

在 module1 徹底加載以前須要先加載 module2,而 module2 的加載又須要 module1。這種狀態下,咱們從 exports 對象中能獲得的就是在發生循環依賴以前的這部分。上面代碼中,只有 a 屬性被引入,由於 b 和 c 都須要在引入 module2 以後才能加載進來。

Node 使這個問題簡單化,在一個模塊加載期間開始建立 exports 對象。若是它須要引入其餘模塊,而且有循環依賴,那麼只能部分引入,也就是隻能引入發生循環依賴以前所定義的這部分。

AMD

AMD 是 Asynchronous Module Definition 的簡稱,即「異步模塊定義」,是從 CommonJS 討論中誕生的。AMD 優先照顧瀏覽器的模塊加載場景,使用了異步加載和回調的方式。

AMD 和 CommonJS 同樣須要腳本加載器,儘管 AMD 只須要對 define 方法的支持。define 方法須要三個參數:模塊名稱,模塊運行的依賴數組,全部依賴均可用以後執行的函數(該函數按照依賴聲明的順序,接收依賴做爲參數)。只有函數參數是必須的。define 既是一種引用模塊的方式,也是定義模塊的方式。

// file lib/sayModule.js
define(function (){
    return {
        sayHello: function () {
            console.log('hello');
        }
    };
});
 
//file main.js
define(['./lib/sayModule'], function (say){
    say.sayHello(); //hello
})

main.js 做爲整個應用的入口模塊,咱們使用 define 關鍵字聲明瞭該模塊以及外部依賴(沒有生命模塊名稱);當咱們執行該模塊代碼時,也就是執行 define 函數的第二個參數中定義的函數功能,其會在框架將全部的其餘依賴模塊加載完畢後被執行。這種延遲代碼執行的技術也就保證了依賴的併發加載。

RequireJS

RequireJS 是一個前端的模塊化管理的工具庫,遵循AMD規範,經過一個函數來將全部所須要的或者說所依賴的模塊實現裝載進來,而後返回一個新的函數(模塊),咱們全部的關於新模塊的業務代碼都在這個函數內部操做,其內部也可無限制的使用已經加載進來的以來的模塊。

<script data-main='scripts/main' src='scripts/require.js'></script>
//scripts下的main.js則是指定的主代碼腳本文件,全部的依賴模塊代碼文件都將從該文件開始異步加載進入執行。

defined用於定義模塊,RequireJS要求每一個模塊均放在獨立的文件之中。按照是否有依賴其餘模塊的狀況分爲獨立模塊和非獨立模塊。
一、獨立模塊 不依賴其餘模塊。直接定義

define({
    methodOne: function (){},
    methodTwo: function (){}
});
 
//等價於
 
define(function (){
    return {
        methodOne: function (){},
        methodTwo: function (){}
    };
});

二、非獨立模塊,對其餘模塊有依賴

define([ 'moduleOne', 'moduleTwo' ], function(mOne, mTwo){
    ...
});
 
//或者
 
define( function( require ){
    var mOne = require( 'moduleOne' ),
        mTwo = require( 'moduleTwo' );
    ...
});

如上代碼, define中有依賴模塊數組的 和 沒有依賴模塊數組用require加載 這兩種定義模塊,調用模塊的方法合稱爲AMD模式,定義模塊清晰,不會污染全局變量,清楚的顯示依賴關係。AMD模式能夠用於瀏覽器環境而且容許非同步加載模塊,也能夠按需動態加載模塊。

CMD

CMD(Common Module Definition),在CMD中,一個模塊就是一個文件。

全局函數define,用來定義模塊。
參數 factory 能夠是一個函數,也能夠爲對象或者字符串。
當 factory 爲對象、字符串時,表示模塊的接口就是該對象、字符串。

定義JSON數據模塊:

define({ "foo": "bar" });

factory 爲函數的時候,表示模塊的構造方法,執行構造方法即可以獲得模塊向外提供的接口。

define( function(require, exports, module) { 
    // 模塊代碼
});

 

SeaJS

sea.js 核心特徵:

  1. 遵循CMD規範,與NodeJS般的書寫模塊代碼。
  2. 依賴自動加載,配置清晰簡潔。

seajs.use用來在頁面中加載一個或者多個模塊

// 加載一個模塊 
seajs.use('./a');
 
// 加載模塊,加載完成時執行回調
seajs.use('./a',function(a){
    a.doSomething();
});
 
// 加載多個模塊執行回調
seajs.use(['./a','./b'],function(a , b){
    a.doSomething();
    b.doSomething();
});

 

AMD和CMD最大的區別是對依賴模塊的執行時機處理不一樣,注意不是加載的時機或者方式不一樣。
不少人說requireJS是異步加載模塊,SeaJS是同步加載模塊,這麼理解其實是不許確的,其實加載模塊都是異步的,只不過AMD依賴前置,js能夠方便知道依賴模塊是誰,當即加載,而CMD就近依賴,須要使用把模塊變爲字符串解析一遍才知道依賴了那些模塊,這也是不少人詬病CMD的一點,犧牲性能來帶來開發的便利性,實際上解析模塊用的時間短到能夠忽略。爲何說是執行時機處理不一樣?
一樣都是異步加載模塊,AMD在加載模塊完成後就會執行該模塊,全部模塊都加載執行完後會進入回調函數,執行主邏輯,這樣的效果就是依賴模塊的執行順序和書寫順序不必定一致,看網絡速度,哪一個先下載下來,哪一個先執行,可是主邏輯必定在全部依賴加載完成後才執行。
CMD加載完某個依賴模塊後並不執行,只是下載而已,在全部依賴模塊加載完成後進入主邏輯,遇到require語句的時候才執行對應的模塊,這樣模塊的執行順序和書寫順序是徹底一致的。

UMD

統一模塊定義(UMD:Universal Module Definition )就是將 AMD 和 CommonJS 合在一塊兒的一種嘗試,常見的作法是將CommonJS 語法包裹在兼容 AMD 的代碼中。

(function(define) {
    define(function () {
        return {
            sayHello: function () {
                console.log('hello');
            }
        };
    });
}(
    typeof module === 'object' && module.exports && typeof define !== 'function' ?
    function (factory) { module.exports = factory(); } :
    define
));

該模式的核心思想在於所謂的 IIFE(Immediately Invoked Function Expression),該函數會根據環境來判斷須要的參數類別

ES6模塊(module)

嚴格模式

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)

模塊Module

一個模塊,就是一個對其餘模塊暴露本身的屬性或者方法的文件。

導出Export

做爲一個模塊,它能夠選擇性地給其餘模塊暴露(提供)本身的屬性和方法,供其餘模塊使用。

// profile.js
export var firstName = 'qiqi';
export var lastName = 'haobenben';
export var year = 1992;
 
//等價於
 
var firstName = 'qiqi';
var lastName = 'haobenben';
var year = 1992;
export {firstName, lastName, year}

一、 一般狀況下,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。它們的實質是,在接口名與模塊內部變量之間,創建了一一對應的關係。

三、最後,export命令能夠出如今模塊的任何位置,只要處於模塊頂層就能夠。若是處於塊級做用域內,就會報錯,接下來講的import命令也是如此。

function foo() {
  export default 'bar' // SyntaxError
}
foo()

 

導入import

做爲一個模塊,能夠根據須要,引入其餘模塊的提供的屬性或者方法,供本身模塊使用。

一、 import命令接受一對大括號,裏面指定要從其餘模塊導入的變量名。大括號裏面的變量名,必須與被導入模塊(profile.js)對外接口的名稱相同。若是想爲輸入的變量從新取一個名字,import命令要使用as關鍵字,將輸入的變量重命名。

import { lastName as surename } from './profile';

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

三、注意,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';
}

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

import 'lodash';
//上面代碼僅僅執行lodash模塊,可是不輸入任何值。

 

默認導出(export default)

每一個模塊支持咱們導出一個沒有名字的變量,使用關鍵語句export default來實現.

export default function(){
            console.log("I am default Fn");
        }
//使用export default關鍵字對外導出一個匿名函數,導入這個模塊的時候,能夠爲這個匿名函數取任意的名字
 
//取任意名字都可
import sayDefault from "./module-B.js";
sayDefault();
//結果:I am default Fn

一、默認輸出和正常輸出的比較

// 第一組
export default function diff() { // 輸出
  // ...
}
 
import diff from 'diff'; // 輸入
 
// 第二組
export function diff() { // 輸出
  // ...
};
 
import {diff} from 'diff'; // 輸入
 
//上面代碼的兩組寫法,第一組是使用export default時,對應的import語句不須要使用大括號;第二組是不使用export default時,對應的import語句須要使用大括號。

 

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

二、由於export default本質是將該命令後面的值,賦給default變量之後再默認,因此直接將一個值寫在export default以後。

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

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

import _, { each } from 'lodash';
 
//對應上面代碼的export語句以下
export default function (){
    //...
}
export function each (obj, iterator, context){
    //...
}

 

export 與 import 的複合寫法

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

export { foo, bar } from 'my_module';
 
// 等同於
import { foo, bar } from 'my_module';
export { foo, bar };
 
/ 接口更名
export { foo as myFoo } from 'my_module';
 
// 總體輸出
export * from 'my_module';

注意事項
一、聲明的變量,對外都是隻讀的。可是導出的是對象類型的值,就可修改。
二、導入不存在的變量,值爲undefined。

ES6 中的循環引用

ES6 中,imports 是 exprts 的只讀視圖,直白一點就是,imports 都指向 exports 本來的數據,好比:

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}
 
//------ main.js ------
import { counter, incCounter } from './lib';
 
// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4
 
// The imported value can’t be changed
counter++; // TypeError

所以在 ES6 中處理循環引用特別簡單,看下面這段代碼:

//------ a.js ------
import {bar} from 'b'; // (1)
export function foo() {
  bar(); // (2)
}
 
//------ b.js ------
import {foo} from 'a'; // (3)
export function bar() {
  if (Math.random()) {
    foo(); // (4)
  }
}

假設先加載模塊 a,在模塊 a 加載完成以後,bar 間接性地指向的是模塊 b 中的 bar。不管是加載完成的 imports 仍是未完成的 imports,imports 和 exports 之間都有一個間接的聯繫,因此老是能夠正常工做。

實例

 

//---module-B.js文件---
//導出變量:name
export var name = "cfangxu";
 
moduleA模塊代碼:
//導入 模塊B的屬性 name    
import { name } from "./module-B.js";   
console.log(name)
//打印結果:cfangxu

批量導出

//屬性name
var name = "cfangxu";
//屬性age
var age  = 26;
//方法 say
var say = function(){
            console.log("say hello");
         }
//批量導出
export {name,age,say}

批量導入

//導入 模塊B的屬性
import { name,age,say } from "./module-B.js";
console.log(name)
//打印結果:cfangxu
console.log(age)
//打印結果:26
say()
//打印結果:say hello

重命名導入變量

import {name as myName} from './module-B.js';
console.log(myName) //cfangxu

總體導入

/使用*實現總體導入
import * as obj from "./module-B.js";
 
console.log(obj.name)
//結果:"cfangxu"
console.log(obj.age)
//結果:26
obj.say();
//結果:say hello
相關文章
相關標籤/搜索