ES6的出現(xiàn),使得我們對Module
的理解停留在export
和import
的使用上拭荤,但整個JavaScript歷史長河里泼菌,Module是怎樣一個演變過程呢,下面就Module的進化過程做一個簡單介紹然痊,涉及的內容較多,每個模塊涉入不深屉符,供大家簡單的了解剧浸。文中有什么說的不對的地方,歡迎提出指正矗钟。覺得內容還行的歡迎點贊唆香,您的鼓勵,是我前進的動力吨艇。
模塊化誕生的初衷:web系統(tǒng)的龐大躬它、復雜、團隊的分工協(xié)作东涡,使得后期維護的成本越來越高冯吓,而模塊化是用于保持代碼塊之間相互獨立而普遍使用的設計模式。
什么是
Module
呢疮跑?
Module
就是將一個復雜的程序依據一定的規(guī)則(規(guī)范)封裝成幾個塊(文件)并進行組合在一起组贺。塊的內部數(shù)據/實現(xiàn)是私有的,只是向外部暴露一些接口(方法)與外部其它模塊通信祖娘。
歷史上失尖,JavaScript一直沒有模塊體系,直到CommonJS
和AMD
的出現(xiàn),下面來看下在ES6之前掀潮,是怎樣將大程序拆分成互相依賴的小文件的菇夸。
模塊化進化史
- 全局function模式 ----- 將不同的功能封裝成不同的函數(shù)
// module1.js
let data = 'module1';
function foo() {
console.log(`foo():${data}`);
}
function bar() {
console.log(`bar():${data}`);
}
// module2.js
let data2 = 'module2';
function foo() { //與另一個模塊中的函數(shù)沖突了
console.log(`foo():${data2}`);
}
// index.html
<body>
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
foo(); // foo():module2 foo()函數(shù)值的輸出與js文件的加載順序有關
bar(); // bar():module1
</script>
</body>
缺點:global變量被污染,容易造成命名沖突胧辽。
2.namespace模式 ----- 簡單對象封裝峻仇,減少了全局變量
// module1.js
let myModule1 = {
data: 'module1.js',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
// module2.js
let myModule2 = {
data: 'module2.js',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
// index.html
<body>
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript">
myModule1.foo()
myModule1.bar()
myModule2.foo()
myModule2.bar()
myModule1.data = 'other data' //能直接修改模塊內部的數(shù)據
myModule1.foo(); // 輸出other data
</script>
</body>
缺點:模塊數(shù)據缺乏獨立性公黑,外部可更改內部數(shù)據邑商。
3.IIFE模式(立即調用函數(shù)表達式)-----匿名函數(shù)自調用
優(yōu)點:數(shù)據是私有的,外部只能通過暴露的方法操作
問題:如果當前模塊依賴另一個模塊怎么辦凡蚜?
// module.js
(function (window) {
let data = 'module'; // 數(shù)據
// 操作數(shù)據的函數(shù)
function foo() { // 用于暴露有函數(shù)
console.log(`foo():${data}`);
}
function bar() { // 用于暴露有函數(shù)
console.log(`bar():${data}`)
otherFun(); // 內部調用
}
function otherFun() { //內部私有的函數(shù)
console.log('otherFun()');
}
//暴露行為
window.myModule = {foo, bar}
})(window)
// index.html
<body>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo(); // foo():module
myModule.bar(); // bar():module otherFun()
//myModule.otherFun() // TypeError:myModule.otherFun is not a function
console.log(myModule.data); // undefined:不能訪問模塊內部數(shù)據
myModule.data = 'xxxx'; // 不能修改的模塊內部的data
myModule.foo(); // foo():module人断,沒有改變
</script>
</body>
4.IIFE增強模式----引入依賴(現(xiàn)代模塊化實現(xiàn)的基石)
// module.js
(function (window, $) {
let data = 'NBA'; // 數(shù)據
// 操作數(shù)據的函數(shù)
function foo() { // 用于暴露有函數(shù)
console.log(`foo():${data}`);
$('body').css('background', 'red');
}
function bar() { // 用于暴露有函數(shù)
console.log(`bar() ${data}`);
otherFun(); // 內部調用
}
function otherFun() { // 內部私有的函數(shù)
console.log('otherFun()');
}
// 暴露行為
window.myModule = {foo, bar};
})(window, jQuery)
// index.html
<body>
// 引入的js必須有一定順序
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
</body>
常見的模塊化規(guī)范
在 ES6
之前,社區(qū)制定了一些模塊加載方案朝蜘,最主要的有CommonJS
和AMD
兩種恶迈。前者用于服務器,后者用于瀏覽器谱醇。由于ES6模塊化的出現(xiàn)暇仲,CommonJS
和AMD
規(guī)范漸漸很少被人使用。下面簡單了解下兩種規(guī)范如何使用副渴。
1.CommonJS----用于服務器端奈附,模塊的加載是運行時同步加載的
- 定義暴露模塊:exports
(1)exports.xxx = value;
(2)module.exports = value;
- 引入模塊:require
(1) 第三方模塊:var module = require('xxx模塊名');
(2) 自定義模塊:var module = require('模塊文件相對路徑');
2.AMD--- 專門用于瀏覽器端,模塊的加載是異步的
- 定義暴露模塊:
define()
(1) 定義沒有依賴的模塊
define(function() {
// 代碼塊
return 模塊;
})
(2) 定義有依賴的模塊
define(['module1', 'module2'], function(m1,m2) {
// 代碼塊
return 模塊;
})
- 引入使用的模塊
require(['module1', 'module2'], function(m1, m2) {
// 使用模塊1和模塊2
})
// 或者使用requirejs引入模塊
requirejs(['module1', 'module2'], function(m1, m2) {
// 使用模塊1和模塊2
})
ES6模塊化
ES6模塊的設計思想是盡量靜態(tài)化煮剧,使得在編譯時就能確定模塊之間的依賴關系斥滤,以及輸入和輸出的變量。ES6模塊功能主要有兩個命令構成:export
和import
勉盅。
下面簡單介紹下這兩個命令的使用方式及注意事項佑颇,更多詳細介紹請參考阮一峰Module的語法。
export命令用于規(guī)定模塊的對外接口草娜,import命令用于輸入其他模塊提供的功能挑胸。
ES6模塊中默認采用嚴格模式,不管你頭部有沒有寫"use strict"
1.export
導出export:作為一個模塊宰闰,它可以選擇性地給其它模塊暴露(提供)自己的屬性和方法茬贵,供其它模塊使用。
// profile.js
// 寫法一
export var firstName = 'zxy';
export var yesr = '2020';
//寫法二 推薦寫法
var firstName = 'zxy';
var year = '2020';
export {firstName, year}
注意事項:
-
export
命令除了輸出變量议蟆,也可輸出函數(shù)或類闷沥; -
export
輸出的變量就是本來的名字,也可通過as關鍵字重命名咐容;
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
-
export
語句輸出的接口與其對應的值是動態(tài)綁定關系舆逃,即通過該接口可以取到模塊內部實時的值。 -
export
命令可以出現(xiàn)在模塊的任何位置,只要處于模塊頂層就可以路狮。如果處于塊級作用域內虫啥,就會報錯。import
命令亦是如此奄妨。這是因為處于條件代碼塊之中涂籽,就沒法做靜態(tài)優(yōu)化了,違背了ES6模塊的設計初衷砸抛。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500); // 輸出變量foo评雌,值為bar,500ms之后變?yōu)閎az
function foo() {
export default 'bar'; // SyntaxError
}
2.import
導入import:作為一個模塊直焙,可以根據需要景东,引入其它模塊提供的屬性或者方法,供自己模塊使用奔誓。
// main.js
import { firstName, year } from './profile.js';
function setName(element) {
element.textContent = firstName ;
}
- 大括號中的變量名必須與被導入模塊(profile.js)對外接口的名稱相同斤吐,位置順序無要求。
- import后面的from指定模塊文件的位置厨喂,可以是相對路徑和措,也可以是絕對路徑
- 如果想為輸入的變量重新取一個名字,要在import命令中使用as關鍵字蜕煌,將輸入的變量重命名派阱。
-
import
命令具有提升效果,會提升到整個模塊的頭部并首先執(zhí)行幌绍。這種行為的本質是颁褂,import
命令是編譯階段執(zhí)行的,在代碼運行之前傀广。
import {firstName as name} from './profile.js';
foo();
import {foo} from 'module';
- 由于
import
是靜態(tài)執(zhí)行的颁独,所以不能使用表達式和變量,只有在運行時才能得到結果的的語法結構伪冰。
// 報錯
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';
}
- 如果多次重復執(zhí)行同一句
import
語句誓酒,那么只會執(zhí)行一次,而不會執(zhí)行多次贮聂。
import 'loadsh';
import loadsh;
- 導入不存在的變量靠柑,值為
undefined
// module1.js
export var name = 'jack';
// module2.js
import {height} from './module1.js';
console.log(height); // 輸出結果:undefined
- 聲明的變量,對外都是只讀的吓懈。請注意下方解釋
// module1.js
export var name = 'jack';
// module2.js
import {name} from './module1.js';
name="修改字符串變量"; // 親試歼冰,不會報錯,輸出:修改字符串變量
這里的對外只讀并不是指
import
聲明的變量都是只讀的耻警,引入后不可改寫隔嫡,而是當定義的接口或變量為只讀時(通過Object.defineProperty()
),import
引入的變量就不可被更改甸怕。
3.模塊的整體加載
- 除了指定加載某個輸出值,還可以使用整體加載(即星號*)來指定一個對象腮恩,所有輸出值都加載在這個對象上梢杭。
// moduleA.js
var name='zxy';
var age = '18';
var say = function() {
console.log('say hello');
}
export {name, age, say}
// moduleB.js
import * as obj from './moduleA.js';
obj.name; // zxy
obj.age; // 18
obj.say(); // say hello
4.export default命令
export default
命令用于指定模塊的默認輸出。顯然秸滴,一個模塊只能有一個默認輸出武契,因此export default
命令只能使用一次。所以import
命令后面不用加大括號荡含,因為只可能對應一個方法咒唆。
// moduleA.js
export default function() {
console.log('zxy')
}
// moduleB.js
import sayDefault from './moduleA.js';
sayDefault(); // zxy
-
export default
就是輸出一個叫作default
的變量或方法,然后系統(tǒng)允許我們?yōu)樗∪我饷帜诳拧J褂胊s關鍵字钧排。 - 因為
export default
命令其實是輸出一個叫default
的變量敦腔,因此它后面不能跟變量聲明語句均澳。
export var a = 1; // 正確
export default var a = 1; // 錯誤
var a = 1; export default a; // 正確
同樣地,因為export default
命令的本質是將后面的值符衔,賦給default
變量找前,所以可以直接將一個值寫在export default
之后。
export default 1; // 正確
export 1; // 錯誤
上面代碼中判族,后一句報錯是因為沒有指定對外的接口躺盛,而前一句指定對外接口為default
。
-
import()方法
-import()
函數(shù)用途完成動態(tài)加載形帮。import()
返回一個Promise
對象槽惫。
function initEntity() {
return Promise.all([
import('./A.js'),
import('./B.js');
]).then([transDefine] => {
// code
})
- 按需加載
btn.addEventListener('click', event =>{
import('./dialogBox.js');
}).then(dialogBox => {
dialogBox.open();
}).catch(error => {
// code
})
- 條件加載
if(condition) {
import('moduleA');
} else {
import('moduleB');
}