JavaScript(4)之——前端模塊化

  談談對前端模塊化的理解是面試時常常會被問到的問題,我以面試者的口吻來寫了如何分步驟回答這道問題。前端

第一步:模塊化是什麼?

  將一個複雜程序安裝必定的規則封裝成幾個塊兒,並組合在一塊兒。塊的內部,數據和函數實現是私有的,只向外部暴露出來一些接口與外部的其餘模塊通訊。node

第二步:模塊化的發展

全局function模式

  把不一樣功能封裝成不一樣的全局函數,污染全局命名空間,容易引發命名衝突或數據不安全,並且模塊成員之間看不出直接關係。git

namespace模式(命名空間)

  簡單對象封裝,減小了全局變量,解決命名衝突,可是數據不安全,外部能夠直接修改模塊內部的數據。es6

IIFE模式:匿名函數自調用(閉包)

  數據是私有的,外部只能經過暴露的方法操做,將數據和行爲封裝到一個函數內部,經過給window添加屬性來向外暴露接口,可是如何解決模塊依賴呢?github

(function(window) {
    let data = "hello world"

    function sayHi() {
        console.log(data)
    }
    window.myModule = { sayHi }
})(window)

myModule.sayHi()

複製代碼
IIFE加強模式:引入依賴
(function(window) {
    const name = 'David'
    window.myModule2 = { name }
})(window);

(function(window, myModule2) {
    let data = "hello world"

    function sayHi() {
        console.log(`${myModule2.name} said: "${data}"`)
    }
    window.myModule = { sayHi }
})(window, myModule2)

myModule.sayHi()
複製代碼
存在的問題
  1. 每一個模塊都須要手動引入,引入過多的script,就會發送過多的請求。
  2. 依賴模糊,很難說清楚具體依賴關係是什麼,也就是說很容易由於不瞭解他們之間的依賴關係致使加載前後順序出錯。
  3. 前兩點問題致使難以維護多個引入的js文件。
  4. 所以須要一個好的模塊化規範來約束咱們實現模塊化的方式。

第三步 模塊化的好處

  1. 避免命名衝突。
  2. 更好的分離,按需加載。
  3. 更高的複用性。
  4. 高可維護性。

第四步 模塊化規範

  如今AMD和CMD已經逐漸退出歷史舞臺,咱們主要介紹經常使用的兩種規範:CommonJS和ES6模塊化。面試

CommonJS

  Node應用中的規範,每一個文件就是一個模塊,有本身的做用域。在文件中定義的變量,函數和類都是私有的,對其餘文件是不可見的。其餘文件只能引用它暴露的接口。在服務端,模塊的加載是運行時同步加載的。瀏覽器

  特色:全部代碼都在其模塊做用域不會污染全局,執行的順序是模塊出現的順序。模塊屢次加載只會在第一次加載時運行一次,而後緩存。緩存

  基本語法:暴露模塊:module.exports = xxx; exports.xxx = value。引入模塊:require(xxx)。安全

  1. 匿名導出:
module.exports = function() {
    console.log('hello world')
}
let sayHi = require('./test')
sayHi()
複製代碼
module.exports = 1
const num = require('./test')
console.log(num)
複製代碼
  1. 具名導出:
let sayHi = function() {
     console.log('hello world')
 }
 let num = 5
 module.exports = {
     sayHi: sayHi,
     num: num
 }  
 
let { sayHi, num } = require('./test')
sayHi()
console.log(num)
複製代碼

  模塊的加載機制:輸入的是輸出值的拷貝,一旦輸出一個值,模塊內部的變化影響不到已經輸出的值。由於它只運行一次,以後都都用的是緩存中的值。babel

ES6模塊化

  ES6 模塊的設計思想是儘可能的靜態化(編譯時加載),使得編譯時就能肯定模塊的依賴關係,以及輸入和輸出的變量。

  特色:一個模塊就是一個獨立的文件,該文件內部的全部變量,外部沒法獲取。若是但願外部可以讀取模塊內部的某個變量,就必須使用export關鍵字輸出該變量。export語句輸出的接口與其對應的值是動態綁定關係,即經過該接口能夠取到模塊內部實時的值。

  基本語法:export和import。export命令用於規定模塊的對外接口,import命令用於輸入其餘模塊提供的功能。

  1. 具名導出的兩種方式
export const firtsName = 'Wang'
export const secondName = 'Lin'

const firtsName = 'Wang'
const secondName = 'Lin'
export { firtsName, secondName }
複製代碼
import { firtsName, secondName } from './test.js'
console.log(firtsName + secondName)
複製代碼

  特別注意,export命令規定的是對外的接口,必須與模塊內部的變量創建一一對應關係。

export 1 //報錯
var m = 1; export m;//報錯
//正確寫法:
export var m = 1;
var m = 1; export {m}
var m = 1; export {n as m}
複製代碼
  1. 匿名導出:export default

  使用import命令時用戶須要知道多要加載的變量名和函數名,不然沒法加載。能夠用export default命令爲模塊指定默認輸出。在import時就能夠爲該匿名函數指定任意名字。

export default function() {
    console.log('hi')
}

import sayHi from './test.js'
sayHi()
複製代碼

  一個模塊只能有一個默認輸出,所以export default就是輸出一個叫作defalut的變量或方法。它只能使用一次,因此import命令後面纔不用加大括號。

  es6模塊在瀏覽器中的加載規則,<script type=」module」 src=」myModule.js」></script>加一個type屬性設爲module,瀏覽器就會認爲它是es6模塊,默認它是異步加載,等同於打開了defer屬性。

  執行機制:遇到模塊加載命令import就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用到被加載的模塊中取值。ES6模塊是動態引用,並不會緩存值,模塊裏面的變量綁定其所在的模塊。因爲ES6輸入的模塊變量只是一個符號連接,因此這個變量是隻讀的,對它從新賦值會報錯。

第五步 commonJs和es6模塊化的區別

  1. CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
  2. CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

第六步 循環加載

  循環加載指的是,a腳本的執行依賴於b腳本,而b腳本的執行又依賴於a腳本。循環加載表示存在強耦合,若是處理很差,還可能致使遞歸加載,使得程序沒法執行。ES6模塊和CommonJS模塊在處理循環加載時的方法是不同的,返回的結果也不同。

CommonJS模塊的加載原理和循環加載

  CommonJS的一個模塊就是一個腳本文件。require命令第一次加載該腳本時就會執行整個腳本,而後在內存中生成一個對象。所以它的以下方法:

let { sayHi, num } = require('./test')
複製代碼

  等同於:

let test = require('./test')
let sayHi = test.sayHi
let num = test.num
複製代碼

  它生成一個對象以後,之後每次用到這個模塊就會在這個對象中取值。不管CommonJS模塊加載多少次,只有第一次加載時會運行一次,之後再加載時返回第一次運行的結果。除非手動清除緩存。

  由於腳本代碼在require的時候就會所有執行,一旦出現某個模塊被循環加載,就只輸出已經執行的部分,還未執行的部分不會輸出。當它循環加載完成,兩個腳本都會被所有執行一遍,所以循環加載的模塊中的值可能會被改寫。

//a.js 
exports.done = false //第二步,設置done屬性爲false並導出
var b = require('./b.js') //第三步, 第一次require加載b.js腳本,進入到b.js中,a.js暫停在這裏。
console.log('在a.js之中,b.done = %j', b.done) //第九步,執行完b.js繼續執行a.js,此時從b.js導出的exports中的最終值done爲true
exports.done = true //第十步,設置done屬性爲true並導出,這是a.js導出的最終值
console.log('a.js執行完畢') //第十一步,a.js執行完畢

//b.js
exports.done = false //第四步,設置done屬性爲false並導出
var a = require('./a.js') //第五步,第二次require加載a.js腳本,不會再運行a.js,直接從內存中exports中取值
console.log('在b.js之中,a.done = %j', a.done) //第六步,因a.js沒有執行完,從exports中取到的值是已經執行的部分,而不是最後的值
exports.done = true //第七步,設置done屬性爲true並導出,這是b.js導出的最終值
console.log('b.js執行完畢') //第八步,b.js執行完畢

//main.js
var a = require('./a.js') //第一步,第一次require加載a.js腳本,進入到a.js中。第十二步,執行完a.js後,能夠獲得a.js的最終值,done爲true
var b = require('./b.js') //第十三步,第二次require加載b.js腳本,不會再運行b.js,直接從內存中exports中取值
console.log('在main.js之中,a.done = %j, b.done = %j', a.done, b.done)// 第十四步,取到最終值輸出。 

//執行main.js
node main.js
//執行結果
在b.js之中,a.done = false
b.js執行完畢
在a.js之中,b.done = true
a.js執行完畢
在main.js之中,a.done = true, b.done = true
複製代碼
ES6模塊循環加載

  由於ES6的模塊是動態引用,變量不會被緩存,而是成爲一個指向被加載模塊的引用。只要引用存在,代碼就能執行。ES6加載的變量都是動態引用其所在模塊的,只要引用存在,代碼就能執行。而CommonJS中require時就會直接加載引用的模塊,可以用到的只有已經執行的部分,若是用到尚未執行的部分就會報錯。

//a.js
import { bar } from './b.js' //第一步,加載b.js,進入b.js
console.log('a.js') //第六步,執行完b.js,開始執行a.js
console.log(bar) //第七步,bar在b.js中的引用爲'bar',輸出bar
export let foo = 'foo' //第八步,具名導出foo的值爲'foo'

//b.js
import { foo } from './b.js' //第二步,加載a.js,這時因爲a.js已經開始執行,因此不會重複執行,繼續執行b.js
console.log('b.js') // 第三步,輸出b.js
console.log(foo) //第四步,輸出foo的值,此時a.js尚未執行完,foo在./b.js中的引用爲undefined,輸出undefined
export let bar = 'bar' //第五步,具名導出bar的值爲'bar'

//執行a.js
babel-node a.js
//執行結果
b.js
undefined
a.js
bar
複製代碼
總結對比

  形成兩種模塊加載方案在處理「循環加載」時不一樣的緣由在於,它們二者加載模塊的不一樣,一個加載拿到的是值的拷貝,一次拿到,不會改變;一個拿到的是值的引用,會隨着執行的過程發生變化。

  • CommonJS只會在第一次require加載時執行一遍腳本,把執行結果exports的值緩存在內存中,以後再require,只會用到內存中的值,內存中的值是腳本中值的一份拷貝。在循環引用時,第一次require,腳本的內容執行中被暫停,內存中就只放已經執行完的部分的exports的值,還未執行的部分不會輸出。若此時exports中尚未值,那麼代碼就會報錯。
  • Import加載時只會生成一個動態引用,而不會將值緩存在內存中。循環加載時,若引用到的值尚未被執行,那麼該引用就不會拿到值,默認值爲undefined,當代碼執行完,就能夠拿到值了。

參考

  github.com/ljianshu/Bl…以及阮一峯《ES6標準入門》

相關文章
相關標籤/搜索