面試官:說說 JS 中的模塊化吧

一門語言發展壯大的畢竟之路就是模塊化的出現,不少語言天生設計中就帶有這一特性,惋惜的是 JS 從一開始就不是天生支持模塊化的,變量污染和命名衝突問題讓衆多 js 開發者不得不尋求一種實現 js 模塊化的方法。從最初的自執行函數,到 AMD / CMD ,再到後來的 Commonjs 規範,以及 ES6 中出現的 ESModule,語言的發展讓咱們能夠選擇不一樣的模塊化技術來應對的不一樣場景。javascript

爲何使用模塊化?

  • 解決變量間相互污染的問題,以及變量命名衝突的問題
  • 提升代碼的可維護性、可拓展性和複用性

自執行函數實現的模塊化

自執行函數實現模塊化的方法很是簡單:html

// 自執行函數實現模塊化
(function () {
    var a = 1;
    console.log(a); // 1
})();

(function () {
    var a = 2;
    console.log(a); // 2
})();
複製代碼

自執行函數本質上是經過函數做用域解決了命名衝突、污染全局做用域的問題。前端

AMD 、CMD 和 UMD

AMD 是"Asynchronous Module Definition"的縮寫,意思就是"異步模塊定義" ,它採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。全部依賴這個模塊的語句,都定義在一個回調函數中,等到全部依賴項都加載完成以後,這個回調函數纔會運行。vue

CMD 是"Common Module Definition"的縮寫,意思就是"公共模塊定義"。CMD 可使用 require 同步加載依賴,也可使用 require.async 來異步加載依賴。java

AMD 和 CMD 都是非官方的兩種 js 模塊化規範,AMD 標準的表明框架是 RequireJS ,CMD 標準的表明框架是 SeaJS。node

// AMD
define(['./a', './b'], function(a, b) {
  // 加載模塊完畢可使用
  a.do();
  b.do();
});

// CMD
define(function(require, exports, module) {
  // 加載模塊
  // 能夠把 require 寫在函數體的任意地方實現延遲加載
  var a = require('./a');
  a.doSomething();
  
  // 也可使用 require.async 來延遲加載
  require.async('./b', function(b) {
    b.doSomething();
  });
});
複製代碼

如今 ESModuleCommonJS 已經分別統一了瀏覽器端和 Node 端的模塊加載, AMDCMD 使用的比較少,不過做爲不少老項目使用的模塊化方案,仍是值得了解一下的。jquery

AMDCMD 相比,很大的一個區別就是引入模塊的時機,AMD 是前置依賴,也就是說,目標模塊代碼執行前,必須保證全部的依賴都被引入而且執行。CMD 是後置依賴,也就是說,只有在目標代碼中手動執行 require(..) 的時候,相關依賴纔會被加載並執行。webpack

還有一個區別就是引入模塊的方式,AMD 的定位是瀏覽器環境,因此是異步引入;而 CMD 的定位是瀏覽器環境和 Node 環境,它可使用 require 進行同步引入,也可使用 require.async 的方式進行異步引入。git

UMD

從下面的代碼中不難看出,其實 UMD 就是一種通用的模塊化方式,它將 AMDCMD 以及全局註冊的方式作了整合而已,咱們熟悉的 jQuery 和不少的工具庫都是使用這種模塊化的方式進行引入。es6

// UMD
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS-like
        module.exports = factory(require('jquery'));
    } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.jQuery);
    }
}(this, function ($) {
    // methods
    function myFunc(){};

    // exposed public method
    return myFunc;
}));
複製代碼

CommonJS 實現模塊化

CommonJS 是的 NodeJS 所使用的一種服務端的模塊化規範,它將每個文件定義爲一個 module ,模塊必須經過 module.exports 導出對外的變量或接口,經過 require() 來導入其餘模塊的輸出到當前模塊做用域中

CommonJS 使用方式

使用方式一:

// a.js
const a = 1;
const func = function () {
    return a + 1;
}
// 將 func 做爲一個模塊導出
module.exports = func;


// main.js
const func = require('./a.js');
console.log(func());
複製代碼

使用方式二:

// a.js
const a = 1;
const func = function () {
    return a + 1;
}
// 將 func 做爲模塊的一個屬性導出
module.exports.func = func;


// main.js
const { func } = require('./a.js');
console.log(func());
複製代碼

使用方式三:

// a.js
const a = 1;
const func = function () {
    return a + 1;
}
// 將 func 做爲模塊的一個屬性導出,等同於上面一中寫法
module.exports = {
    func
};


// main.js
const { func } = require('./a.js');
console.log(func());
複製代碼

使用方式四:

// a.js
const a = 1;
const func = function () {
    return a + 1;
}
// 利用 Node 提供的便捷寫法 exports 來導出模塊
exports.func = func;


// main.js
const { func } = require('./a.js');
console.log(func());
複製代碼

CommonJS 特色

CommonJS 具備以下特色:

  • 全部代碼都運行於模塊做用域,不會污染全局。
  • 使用同步的方式加載,也就是說,只有加載完成才能執行後面的操做,這點和 AMD 不一樣,因爲 CommonJS 的模塊化是用在 Node 端也就是服務端,模塊加載的時間損耗只是磁盤讀取,這個加載速度是很快的,因此可使用同步的方式。
  • CommonJS 支持動態導入的方式,,好比:require(`./${path}.js`)
  • 模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後加載結果會被緩存,後面再次加載會直接讀取緩存結果,若是想讓模塊從新執行,就必須清除緩存。
  • CommonJS 模塊輸出的是一個**值的拷貝,**這一點會在下面的 ESModule 和 CommonJS 對比中詳細說明。
  • 模塊的加載順序,按照其在代碼中出現的順序。

咱們能夠來模擬一個簡化版 CommonJS 的實現:

  1. 每個模塊內部都有一個 module 對象,表明當前模塊,它須要具備如下屬性:

    • module.id 模塊的惟一標識符
    • module.filename 模塊的文件名、
    • module.loaded 返回一個布爾值,表明模塊是否加載完成
    • module.parent 返回一個對象,表明調用該模塊的父模塊
    • module.children 返回一個數組,內容爲這個模塊所依賴的其餘模塊
    • module.exports 最重要的一個,表示模塊的對外輸出內容
    function Module (id, parent, children) {
        this.id = id;
        this.filename = 'filename.js';
        this.loaded = false;
        this.parent = parent;
        this.children = children;
        this.exports = {};
        // ...
    }
    
    const module = new Module('uuid', null, []);
    複製代碼
  2. Node會爲每個模塊提供一個 export 變量,指向 module.exports

    function Module (id, parent, children) {
        this.id = id;
        this.filename = 'filename.js';
        this.loaded = false;
        this.parent = parent;
        this.children = children;
        this.exports = {};
        // ...
    }
    
    const module = new Module('uuid', null, []);
    let exports = module.exports;
    複製代碼
  3. 模塊開發者向外部導入數據:

    function Module (id, parent, children) {
        this.id = id;
        this.filename = 'filename.js';
        this.loaded = false;
        this.parent = parent;
        this.children = children;
        this.exports = {};
        // ...
    }
    
    const module = new Module('uuid', null, []);
    let exports = module.exports;
    
    module.exports = function () {
        console.log('module message');
    }
    複製代碼

    **注意:**雖然 Node 原生提供了 exports 做爲 module.exports 的簡化寫法,可是不能手動改變 exports 的賦值,好比這樣:exports = {},這樣寫就表明將 module.exports 的引用從 exports 上切斷了。這就意味着:若是一個模塊的對外接口是一個單一的值(例如:數字、函數、字符串),就不能使用 exports 只能使用 module.exports 輸出 。

CommonJS require

CommonJSrequire 的基本功能,是讀入並執行一個 JavaScript 文件,而後返回該模塊的 exports 對象,若是沒有發現指定模塊則報錯。

  • require 加載文件時,默認後綴爲 .js 後綴。
  • 若是 require 中的路徑字符串參數以 '/' 開頭,則會按照這個絕對路徑查找文件。
  • 若是 require 中的路徑字符串參數以 './' 開頭,則會以當前執行腳本位置爲起點,尋找對應的相對路徑下的文件。
  • 若是參數字符串不以 '/' 或者 './' 開頭,則會去尋找一個默認提供的核心模塊(位於 Node 系統安裝目錄中),或者一個位於各級 node_modules 目錄中的已安裝模塊(全局安裝或者局部安裝),舉例來講,若是腳本 '/home/user/projects/foo.js' 執行了 require('bar.js') 命令,Node 會依次搜索如下文件:
    • /usr/local/lib/node/bar.js(Node 的核心模塊)
    • /home/user/projects/node_modules/bar.js(當前執行腳本所在目錄下的 node_modules 文件)
    • /home/user/node_modules/bar.js(執行腳本所在目錄下沒有 node_modules ,則繼續查找上層文件夾的 node_modules)
    • /home/node_modules/bar.js(繼續查找上層的 node_modules)
    • /node_modules/bar.js(最後查找全局的 node_modules)
  • 若是參數字符串不以「./「或」/「開頭,並且是一個路徑,好比require('example-module/path/to/file'),則將先找到example-module的位置,而後再以它爲參數,找到後續路徑。
  • 若是指定的文件沒有找到,Node 會爲文件名添加 .js / .json / .node 後綴再次嘗試匹配,.json 文件會以 JSON 格式的文本文件解析,.node 文件會以編譯後的二進制文件解析。

ESModule 實現的模塊化

ESModule 是 ES6 提供的官方 js 模塊化方案。目前瀏覽器還不能全面支持 ESModule 的語法,須要用 babel 進行解析。

ESModule 經常使用語法

輸出變量和函數接口:

// lib.js
export var a = 1;
// 或者 export 函數
export function func () {};


// main.js
import { a, func } from './lib.js';
複製代碼

將內部變量函數等封裝爲一個對象輸出:

// lib.js
var a = 1;
function func () {}
export {a, func};


// main.js
import { a, func } from './lib.js';
複製代碼

將內部變量函數等更名後封裝爲一個對象輸出:

// lib.js
var a = 1;
function func () {}
// 改一個名字而後暴露
export {a as aa, func as foo};


// main.js
// 注意,這邊引入的時候就必需要用更改後的名字
import { aa, foo} from './lib.js';
複製代碼

export 輸出變量和函數接口,而後在另外一個文件中使用 import * 的方式接收:

// lib.js
export var a = 1;
// 或者 export 函數
export function func () {};


// main.js
import * from './lib.js';
console.log(a);
func();

// 或者這麼接
import * as lib from './lib.js';
console.log(lib.a);
lib.func();

複製代碼

怕麻煩的話,你也能夠直接輸出一個 default 默認。 export default 命令用於指定模塊的默認輸出,一個模塊只能有一個默認輸出,所以export default 命令只能使用一次。因此,import 命令後面不用加大括號,由於只可能惟一對應export default 命令:

// lib.js
export default function () {
    console.log('Hello ESModule');
}


// main.js
// 若是引入 default 默認值,就沒有固定的名稱了,叫什麼均可以
import foo from './lib.js';
複製代碼

**須要注意!**因爲 export 導出的必須是接口,下面的寫法會報錯:

// export 錯誤寫法,由於導出的不是接口而是值
var a = 1;
function func () {}
// 報錯
export a;
// 報錯
export 1;
// 報錯
export func;
複製代碼

可是 export default 例外,由於本質上,export default 就是輸出一個叫作default的變量或方法,而後系統容許你爲它取任意名字。因此,上面那種會報錯的寫法改寫成 default 是有效的,不過注意,一個文件只能暴露一個 default

var a = 1;
export default a;
複製代碼

最後,還有一種 import 和 export 結合的高級寫法,傳說中的模塊繼承,這種寫法咱們會在某些源碼的 main.js 中見到:

// app.vue
function vueComponent () {}
export { vueComponent };


// main.js
export { vueComponent } from './app.vue';
// 或者
export { vueComponent as newVueComponent } from './app.vue';
// 或者
export * from './app.vue';
// 或者
export * as components from './app.vue'; 
複製代碼

default 版本的模塊繼承:

// app.vue
function vueComponent () {}
export default vueComponent;


// main.js
export { default } from './app.vue';
複製代碼

ESModule VS CommonJS

ESModule 對比 CommonJS 主要有如下不一樣:

  • CommonJS 模塊輸出的是一個值的拷貝ESModule 輸出的是值的引用

    CommonJS 輸出的是值的拷貝,也就是說一旦輸出,模塊內部的變化就影響不到這個值

    // lib.js
    let counter = 3;
    function incCounter() {
      counter++;
    }
    module.exports = {
      counter,
      incCounter,
    };
    
    
    // main.js
    let mod = require('./lib');
    
    console.log(mod.counter);  // 3
    mod.incCounter();
    console.log(mod.counter); // 4
    複製代碼

    mod.counter是一個原始類型的值,會被緩存。除非寫成一個函數,才能獲得內部變更後的值。

    // lib.js
    let counter = 3;
    function incCounter() {
        counter++;
    }
    module.exports = {
        get counter: {
            return counter;
        },
        incCounter
    };
    
    
    // main.js
    let mod = require('./lib');
    
    console.log(mod.counter);  // 3
    mod.incCounter();
    console.log(mod.counter); // 3
    複製代碼

    固然,你也能夠對外暴露一個對象,CommonJS 導出的是對象引用的值的複製,那麼這種狀況 ,也是可以獲得內部變更的值的。

    // lib.js
    let obj = {a: 1};
    function changeA() {
        obj.a = 2;
    }
    module.exports = {
        obj,
        changeA
    };
    
    
    // main.js
    const mod = require('./lib.js');
    console.log(JSON.stringify(mod.obj)); // {"a":1}
    target.changeA();
    console.log(JSON.stringify(mod.obj)); // {"a":2}
    複製代碼

    ESModule 輸出的是值的引用,它不會緩存運行結果,而是動態地去被加載的模塊取值,而且變量老是綁定其所在的模塊:

    // 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
    複製代碼
  • ESModule 的模塊化是靜態的,和 CommonJS 不一樣,ESModule 模塊不是對象,而是經過 export 命令顯示輸出的指定代碼的片斷,再經過 import 命令將代碼命令輸入。也就是說在編譯階段就須要肯定模塊之間的依賴關係,這一點不一樣於 AMD / CMD / CommonJS ,這三者都是在運行時肯定模塊間的依賴關係的。

ESModule 的其餘細節特色

  • ES6 的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict";

  • ESModule 導出的模塊是隻讀的,不能變動,不然報錯:

    // lib.js
    export let obj = {};
    
    // main.js
    import { obj } from './lib';
    
    obj.prop = 123; // OK
    obj = {}; // TypeError
    複製代碼

本篇文章已收錄入 前端面試指南專欄

相關參考

往期內容推薦

  1. 完全弄懂節流和防抖
  2. 【基礎】HTTP、TCP/IP 協議的原理及應用
  3. 【實戰】webpack4 + ejs + express 帶你擼一個多頁應用項目架構
  4. 瀏覽器下的 Event Loop
  5. 瀏覽器下的 Event Loop
  6. 面試官:說說原型鏈和繼承吧
相關文章
相關標籤/搜索