一門語言發展壯大的畢竟之路就是模塊化的出現,不少語言天生設計中就帶有這一特性,惋惜的是 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 是"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();
});
});
複製代碼
如今 ESModule
和 CommonJS
已經分別統一了瀏覽器端和 Node 端的模塊加載, AMD
和 CMD
使用的比較少,不過做爲不少老項目使用的模塊化方案,仍是值得了解一下的。jquery
AMD
和 CMD
相比,很大的一個區別就是引入模塊的時機,AMD
是前置依賴,也就是說,目標模塊代碼執行前,必須保證全部的依賴都被引入而且執行。CMD
是後置依賴,也就是說,只有在目標代碼中手動執行 require(..)
的時候,相關依賴纔會被加載並執行。webpack
還有一個區別就是引入模塊的方式,AMD
的定位是瀏覽器環境,因此是異步引入;而 CMD
的定位是瀏覽器環境和 Node 環境,它可使用 require
進行同步引入,也可使用 require.async
的方式進行異步引入。git
從下面的代碼中不難看出,其實 UMD
就是一種通用的模塊化方式,它將 AMD
和 CMD
以及全局註冊的方式作了整合而已,咱們熟悉的 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
是的 NodeJS 所使用的一種服務端的模塊化規範,它將每個文件定義爲一個module
,模塊必須經過module.exports
導出對外的變量或接口,經過require()
來導入其餘模塊的輸出到當前模塊做用域中
使用方式一:
// 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
具備以下特色:
AMD
不一樣,因爲 CommonJS
的模塊化是用在 Node 端也就是服務端,模塊加載的時間損耗只是磁盤讀取,這個加載速度是很快的,因此可使用同步的方式。CommonJS
支持動態導入的方式,,好比:require(`./${path}.js`)
CommonJS
模塊輸出的是一個**值的拷貝,**這一點會在下面的 ESModule 和 CommonJS 對比中詳細說明。咱們能夠來模擬一個簡化版 CommonJS
的實現:
每個模塊內部都有一個 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, []);
複製代碼
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;
複製代碼
模塊開發者向外部導入數據:
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
的基本功能,是讀入並執行一個 JavaScript 文件,而後返回該模塊的exports
對象,若是沒有發現指定模塊則報錯。
.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
的位置,而後再以它爲參數,找到後續路徑。.js / .json / .node
後綴再次嘗試匹配,.json
文件會以 JSON 格式的文本文件解析,.node
文件會以編譯後的二進制文件解析。
ESModule
是 ES6 提供的官方 js 模塊化方案。目前瀏覽器還不能全面支持ESModule
的語法,須要用 babel 進行解析。
輸出變量和函數接口:
// 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
對比 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
,這三者都是在運行時肯定模塊間的依賴關係的。
ES6 的模塊自動採用嚴格模式,無論你有沒有在模塊頭部加上"use strict";
ESModule 導出的模塊是隻讀的,不能變動,不然報錯:
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
複製代碼
本篇文章已收錄入 前端面試指南專欄