隨着JavaScript開發變得越來越普遍,命名空間和依賴性變得越來越難以處理。前端開發者都以模塊化的方式處理該問題。在這篇文章中,我們將探討前端開發人員目前使用的模塊化方案以及試圖解決的問題。
為什么需要JavaScript模塊?
模塊化可以使你的代碼低耦合,功能模塊直接不相互影響。
- 可維護性:每個模塊都是單獨定義的,之間相互獨立。模塊盡可能的需要和外部撇清關系,方便我們獨立的對其進行維護與改造。維護一個模塊比在全局中修改邏輯判斷要好的多。
- 命名空間:為了避免在JavaScript中的全局污染,我們通過模塊化的方式利用函數作用域來構建命名空間。
- 可復用性:雖然粘貼復制很簡單,但是考慮到我們之后的維護以及迭代,你會相當崩潰。
模塊化的解決方案有哪些?
講完了JavaScript模塊化的好處,我們來看下有哪些解決方案來實現JavaScript的模塊化。
揭示模塊模式(Revealing Module)
var myRevealingModule = (function () {
var privateVar = "Ben Cherry",
publicVar = "Hey there!";
function privateFunction() {
console.log( "Name:" + privateVar );
}
function publicSetName( strName ) {
privateVar = strName;
}
function publicGetName() {
privateFunction();
}
// Reveal public pointers to
// private functions and properties
return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
})();
myRevealingModule.setName( "Paul Kinlan" );
通過這種構造,我們通過使用函數有了自己的作用域或“閉包”。
這種方法的好處在於,你可以在函數內部使用局部變量,而不會意外覆蓋同名全局變量,但仍然能夠訪問到全局變量。
優點:
- 可以在任何地方實現(沒有庫,不需要語言支持)。
- 可以在單個文件中定義多個模塊。
缺點:
- 無法以編程方式導入模塊(除非使用eval)。
- 需要手動處理依賴關系。
- 無法異步加載模塊。
- 循環依賴可能很麻煩。
- 很難通過靜態代碼分析器進行分析。
CommonJS
CommonJS是一個旨在定義一系列規范的項目,以幫助開發服務器端JavaScript應用程序。CommonJS團隊試圖解決的一個領域就是模塊。Node.js開發人員最初打算遵循CommonJS規范,但后來決定反對它。
在 CommonJS 的規范中,每個 JavaScript 文件就是一個獨立的模塊上下文(module context),在這個上下文中默認創建的屬性都是私有的。也就是說,在一個文件定義的變量(還包括函數和類),都是私有的,對其他文件是不可見的。
需要注意的一點是,CommonJS以服務器優先的方式來同步載入模塊,假使我們引入三個模塊的話,他們會一個個地被載入。
// In circle.js
const PI = Math.PI;
exports.area = (r) => PI * r * r;
exports.circumference = (r) => 2 * PI * r;
// In some file
const circle = require('./circle.js');
console.log( `The area of a circle of radius 4 is ${circle.area(4)}`);
Node.js的模塊系統通過library的方式對CommonJS的基礎上進行了模塊化實現。
在Node和CommonJS的模塊中,基本上有兩個與模塊系統交互的關鍵字:require和exports。
require是一個函數,可用於將接口從另一個模塊導入當前范圍。傳遞給的參數require是模塊的id。在Node的實現中,它是node_modules目錄中模塊的名稱(或者,如果它不在該目錄中,則是它的路徑)。
exports是一個特殊的對象:放入它的任何東西都將作為公共元素導出。
Node和CommonJS之間的一個獨特區別在於module.exports對象的形式。
在Node中,module.exports是導出的真正特殊對象,而exports它只是默認綁定到的變量module.exports。
另一方面,CommonJS沒有任何module.exports對象。實際意義是,在Node中,無法通過以下方式導出完全預構造的對象module.exports:
// This won't work, replacing exports entirely breaks the binding to
// modules.exports.
exports = (width) => {
return {
area: () => width * width
};
}
// This works as expected.
module.exports = (width) => {
return {
area: () => width * width
};
}
優點
- 簡單:開發人員可以在不查看文檔的情況下掌握概念。
- 集成了依賴管理:模塊需要其他模塊並按所需順序加載。
require
可以在任何地方調用:模塊可以通過編程方式加載。- 支持循環依賴。
缺點
- 同步API使其不適合某些用途(客戶端)。
- 每個模塊一個文件。
- 瀏覽器需要加載程序庫或轉換。
- 模塊沒有構造函數(Node支持)。
- 很難進行靜態代碼分析。
AMD
AMD誕生於一群對CommonJS的研究方向不滿的開發人員。事實上,AMD在開發早期就與CommonJS分道揚鑣,AMD和CommonJS之間的主要區別在於它支持異步模塊加載。
//Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {
//Define the module value by returning a value.
return function () {};
});
// Or:
define(function (require) {
var dep1 = require('dep1'),
dep2 = require('dep2');
return function () {};
});
通過使用JavaScript的傳統閉包來實現異步加載:
在請求的模塊加載完成時調用函數。模塊定義和導入模塊由同一個函數承載:定義模塊時,其依賴關系是明確的。因此,AMD加載器可以在運行時具有項目的模塊依賴圖。因此可以同時加載彼此不依賴的庫。這對於瀏覽器尤其重要,因為啟動時間對於良好的用戶體驗至關重要。
優點
- 異步加載(更好的啟動時間)。
- 支持循環依賴。
- require和的兼容性exports。
- 完全整合了依賴管理。
- 如有必要,可以將模塊拆分為多個文件。
- 支持構造函數。
- 插件支持(自定義加載步驟)。
缺點
- 語法稍微復雜一些。
- 除非編譯,否則需要加載程序庫。
- 很難分析靜態代碼。
除了異步加載以外,AMD的另一個優點是你可以在模塊里使用對象、函數、構造函數、字符串、JSON或者別的數據類型,而CommonJS只支持對象。
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模塊
支持JavaScript標准化的ECMA團隊決定解決模塊問題,
兼容同步和異步操作模式。
//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
ES6 模塊的設計思想是盡量的靜態化,使得編譯時就能確定模塊的依賴關系,以及輸入和輸出的變量。CommonJS 和 AMD 模塊,都只能在運行時確定這些東西。
由於 ES6 模塊是編譯時加載,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入宏(macro)和類型檢驗(type system)這些只能靠靜態分析實現的功能。除了靜態加載帶來的各種好處,ES6 模塊還有以下好處。
- 不再需要UMD模塊格式了,將來服務器和瀏覽器都會支持 ES6 模塊格式。目前,通過各種工具庫,其實已經做到了這一點。
- 將來瀏覽器的新 API 就能用模塊格式提供,不再必須做成全局變量或者navigator對象的屬性。
- 不再需要對象作為命名空間(比如Math對象),未來這些功能可以通過模塊提供。
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。
export
export語法被用來創建JavaScript模塊。你可以用它來導出對象(包括函數)和原始值(primitive values)。導出有兩種類型:named和default。
// named
// lib.js
export function sum(a, b) {
return a + b;
}
export function substract(a, b) {
return a - b;
}
function divide(a, b) {
return a / b;
}
export { divide };
// default
// dog.js
export default class Dog {
bark() {
console.log('bark!');
}
}
import
import語句用來導入其他模塊。
整個導入
// index.js
import * as lib from './lib.js'; console.log(lib.sum(1,2)); console.log(lib.substract(3,1)); console.log(lib.divide(6,3));
導入一個或多個named導出
// index.js
import { sum, substract, divide } from './lib';
console.log(sum(1,2));
console.log(substract(3,1));
console.log(divide(6,3));
需要注意,相應的導入導出名字必須匹配。
導入一個default導出
// index.js
import Dog from './dog.js';
const dog = new Dog();
dog.bark(); // 'bark!'
注意,defualt導出在導入時,可以用任意的名字。所以我們可以這樣做:
import Cat from './dog.js';
const dog = new Cat();
dog.bark(); // 'bark!'
參考
- 很全很全的JavaScript的模塊講解 https://segmentfault.com/a/1190000012464333#articleHeader9
- JavaScript Module Systems Showdown: CommonJS vs AMD vs ES2015 https://auth0.com/blog/javascript-module-systems-showdown/
- ECMAScript 6 入門http://es6.ruanyifeng.com/#docs/module