模擬實現AMD模塊化規範

引子

本文最後的目的是模擬實現AMD模塊化規範,而寫下本文的原因是今天閱讀到了《你不知道的JavaScript–上卷》中作用域閉包的章節,讓我對閉包又有了更深入的理解。

對於閉包的相關知識我之前也根據自己所學到的進行了較詳細的總結,大家可以先來看看這篇文章先,寫的不好的地方多多見諒,更歡迎提出意見和建議

就將這篇文章作為對閉包相關知識的加深深入,廢話不多說,全篇開始!

再談什麼是閉包(閉包的產生)?

我的這篇文章中對閉包的解釋是:

  • 函數互相嵌套
  • 當內部函數引用外部函數中的變量時閉包就產生了,而這個內部函數就是閉包,它並不需調用就能成為閉包

那在我閱讀書籍後,是否推翻了這套結論呢? 恰恰不是,這套結論總結的非常的好,它讓我能夠迅速判別當前的函數是否是閉包, 但作為學術上或者說真正比較官方的對閉包概念解釋上,這樣顯得仍不夠

所以以前的這套結論就來用作迅速判別閉包,而本文就來記錄、總結閉包的完整理解, 記住: 並不是以前的結論不對,而是不夠深入

先總結出閉包最終的完整理解,再不斷的對其進行解釋

所謂閉包,就是:

  • 當函數不僅能夠在定義時所在的詞法作用域以外進行調用
  • 調用時還能正常訪問定義時的詞法作用域

詞法作用域看着很眼熟,不明白什麼意思? 沒關係

詞法作用域

JavaScript中作用域就是指: 用來管理引擎是如何在當前作用域或者嵌套的父子作用域中根據標識符進行變量和變量值查找的一套規則

一提到作用域,我們大多數可以能就會想到: 哦! 全局作用域、函數作用域、變量查找與作用域鏈,甚至塊作用域。但是我想提前說的是: 詞法作用域這些概念息息相關,它屬於是這些概念的集合。一個更加上層的概念

作用域有兩種模型: 詞法作用域和動態作用域

JavaScript中主要的是詞法作用域,但是也有動態作用域的身影,由於本節主要介紹的是詞法作用域,所以不會涉及動態作用域

之所以將作用域命名為詞法作用域,是因為這個概念和JavaScript工作流程中的編譯流程息息相關。雖然JavaScript作為一門動態語言不需要像java和C等語言那樣需要在執行前手動編譯,但不代表它沒有這一套過程

編譯階段最重要的一個工作就是詞法化(單詞化), 它對源代碼進行分析,然後賦予單詞和代碼塊含義,作用域是在詞法階段定義的(在編寫代碼階段),因此當編譯器進行詞法分析時就會保持作用域不變(至少大部分情況是這樣的)

既然作用域與詞法化關聯這麼深,所以就命名為詞法作用域了

總結來說: 詞法作用域是指作用域由代碼書寫時各種變量和函數的位置所決定,編譯的詞法階段通過作用域大概預測出執行過程中如何進行查找

回到閉包

對詞法作用域做了一大段枯燥的解釋後,終於可以回到閉包了。閉包就是基於詞法作用域書寫代碼時所產生的自然結果,它並不是一種新的語法或技術。

當函數能夠記住並訪問所在的詞法作用域時,閉包就產生了,不管函數是否在當前詞法作用域之外執行 這時候回到上面對閉包完整總結那細細品一品先吧

// 一個最典型的閉包
function foo() {
    var a = 2
    function bar() {
        console.log(a)
    }
    return bar
}
var baz = foo()
baz()   // 2

根據詞法作用域的概念,更細來說是作用域。 bar函數擁有一個覆蓋foo函數的作用域,在當前來說bar的作用域範圍是最為之廣闊的,bar能夠訪問foo的作用域甚至全局作用域,用圖來表達一下我說的意思

foo執行後將返回值賦值給全局變量baz,調用baz實際上是根據引用傳遞去調用內部的這個bar而已。根據JS的垃圾回收機制在foo執行完畢後其內部所有的變量和函數都會被回收,但是由於回收機制不會對仍有引用的對象進行回收,所以bar作用域仍然存在。

在回收機制和詞法作用域是靜態確定的相互作用下,baz不僅能夠正常調用,還能夠正常訪問baz引用着的那個函數(bar函數)在定義時的詞法作用域 於是乎閉包就這樣產生了,再回到前面一開始我給出的閉包的完整理解,大家應該就明白是怎麼個意思了

我對閉包概念的再一次深入理解,對閉包概念的深入總結到這就結束了

那閉包有什麼用呢? 書中作者的經驗是,閉包最大的作用就是對當今JavaScript模塊化規範的貢獻。模塊化正是利用閉包發揮出了強大的威力,對於JavaScript模塊化可以看看我的這篇學習筆記

利用閉包編寫模塊

模塊擁有兩個必要條件:

  1. 外部必須是一個函數,且函數必須至少被調用一次(每次調用產生的閉包作為新的模塊實例)
  2. 外部函數內部至少有一個內部函數, 內部函數用於修改和訪問各種內部私有成員

利用閉包編寫一個模塊(例子)

function myModule (){
    const moduleName = '我的自定義模塊'
    var name = 'Fitz'

    // 在模塊內定義方法(API)
    function getName(){
        console.log(name)
    }
    function modifyName(newName){
        name = newName
    }

    // 模塊暴露:  向外暴露API
    return {
        getName,
        modifyName
    }
}

// 測試
const md = myModule()
md.getName()    // 'Fitz'
md.modifyName('LX')
md.getName()    // 'LX'

// 模塊實例之間互不影響
const md2 = myModule()
md2.sayHello = function () {
    console.log('hello')
}
console.log(md) // {getName: ƒ, modifyName: ƒ}

當一個模塊確定只需要一個模塊實例的時候,我們就可以通過IIFE創建,這種方式我們成為單例模式

var singleSample = (function Module (){
    const moduleName = '我的自定義模塊'
    var name = 'Fitz'

    // 在模塊內定義方法(API)
    function getName(){
        console.log(name)
    }
    function modifyName(newName){
        name = newName
    }

    // 模塊暴露:  向外暴露API
    return {
        getName,
        modifyName
    }
})()

console.log(singleSample)   // {getName: ƒ, modifyName: ƒ}

實現AMD模塊化規範

介紹了詞法作用域、閉包這些概念夯實了基礎, 通過使用閉包實現簡單模塊的創建懂的了原理的實際運用,是時候朝着本篇的目標進發了!

AMD規範的語法是大概這樣的

暴露模塊

// 暴露沒有依賴的模塊
define(function () {
    // do something

    return 模塊
})

// 暴露有依賴的模塊
define(
    ['依賴1','依賴2'],
    function (m1, m2) {
        // do something

        return 模塊
    }
)

引入模塊

requirejs(
    ['依賴'],
    function (m1) {
        // do something
    }
)

我們模擬實現這些功能

// IIFE命名的原因是: 無論是否為匿名函數都應該為其取名, 達到見字知意
// ModuleManager是模塊管理器, 它有用於定義模塊和暴露模塊的API, 其本身就是一個模塊
const ModuleManager = (function Fake_AMD_Module_Standard() {

    // 使用者所用定義的模塊對象都會儲存在這裡
    /* 
        最終結果會是
            modules = {
                moduleID: 使用者向外暴露的模塊對象
            }
    */
    let modules = {}

    // 用於定義模塊的API
    /* 
        @parms{
            moduleID: String,
            depends:  Array,
            implement: Function
        }

        分別是: 模塊名字, 依賴對象組成的數組, 使用者定義的模塊
    */
    function define(moduleID, depends, implement) {
        // 當模塊有依賴對象時
        if (depends && implement) {
            // 需要將依賴數組內的所有模塊名替換成實際的對應的模塊
            depends.forEach((moduleID, index)=>{
                depends[index] = __getModule(moduleID)
                /* 
                    原: depends => ['foo', 'bar', 'baz']

                    替換後: depends => [ {say: f}, {test: f, talk: f}, {getName: f} ]
                */
            })
        }
        // 當沒有依賴對象時, 可以省略數組
        // 有依賴的模塊(implement)想要使用依賴內的各種API,必須通過apply將依賴注入到使用者當前定義的模塊(implement)中
        modules[moduleID] = implement? implement.apply(implement, depends) : depends()
        /* 
            depends內的所有模塊對象, 最終會分別被implement中定義的形參所接收
        */
    }



    // 該私有方法用於獲得modules中的模塊對象
    function __getModule(moduleID) {
        return modules[moduleID]
    }


    // 定義用於引入模塊的API
    /* 
        @parms{
            depends: Array
            moduleID: String
        }
    */
    function requireJS(requireModeles, implement) {
        requireModeles.forEach((eachModule, index)=>{
            requireModeles[index] = __getModule(eachModule)
        })
        implement.apply(implement, requireModeles)
    }


    let publicAPI = {
        define,
        requireJS,
    }

    /* 
    為了讓一切更加自然(可以直接在全局調用, 而不需要經過模塊管理器調用API)
    模仿JQuery的方式向全局中也暴露模塊管理器的API
    */
   
   for (const api in publicAPI) {
       if (Object.hasOwnProperty.call(publicAPI, api)) {
           window[api] = publicAPI[api]
        }
    }
    
    return publicAPI
})()




// =============================測試==========================

// 定義一個沒有依賴的模塊
ModuleManager.define('foo', function() {
    function getParm(parm) {
        console.log('我是foo模塊')
        return `得到實參 => ${parm}`
    }
    
    // 向外暴露一個對象, 包含所有需要暴露的API
    return {
        getParm
    }
})

// 沒有引入其他模塊
ModuleManager.requireJS(['foo'], function(foo) {
    console.log(foo)
    foo.getParm()
})



// 定義一個有依賴的模塊
define('sayUtil', ['foo'], function(fooDepend) {
    function sayName(name) {
        let result = fooDepend.getParm(name)
        return `
        <成功測試有依賴的模塊>
        ${result}
        `
        
        /* 
            sayName('啊達')預計返回結果:
                `
                    '我是foo模塊'
                    <成功測試有依賴的模塊>
                    得到實參 => 啊達
                `
        */
    }
    
    // 向外暴露本模塊的API
    return {
        sayName
    }
})

requireJS(['sayUtil'], function(sayUtil) {
    var result = sayUtil.sayName('啊達')
    console.log(result)
})


// 模擬引入其他庫, 再使用模塊
requireJS(['foo', 'sayUtil'], function(foo, $) {
    foo.getParm()
    console.log($.sayName())
})

// =============================測試==========================

寫在最後

文章到這就結束了, 模擬實現只能對最簡單的功能進行模擬

文章看起來有點啰嗦(尤其是前面),寫的不好的地方希望大家見諒