1. 程式人生 > >模擬實現AMD模組化規範

模擬實現AMD模組化規範

[toc] ## 引子 本文最後的目的是**模擬實現AMD模組化規範**,而寫下本文的原因是今天閱讀到了[《你不知道的JavaScript--上卷》](https://www.amazon.cn/dp/B0153179VI)中作用域閉包的章節,讓我對閉包又有了更深入的理解。 對於閉包的相關知識我之前也根據自己所學到的進行了較詳細的總結,大家可以先來看看[這篇文章](https://www.cnblogs.com/fitzlovecode/p/jsadvanced9.html)先,寫的不好的地方多多見諒,更歡迎提出意見和建議 就將這篇文章作為對閉包相關知識的加深深入,廢話不多說,全篇開始! ## 再談什麼是閉包(閉包的產生)? [我的這篇文章](https://www.cnblogs.com/fitzlovecode/p/jsadvanced9.html)中對閉包的解釋是: - 函式互相巢狀 - 當內部函式引用外部函式中的變數時閉包就產生了,而這個內部函式就是閉包,它並不需呼叫就能成為閉包 那在我閱讀書籍後,是否推翻了這套結論呢? ==恰恰不是,這套結論總結的非常的好,它讓我能夠迅速判別當前的函式是否是閉包==, 但作為學術上或者說真正比較官方的對閉包概念解釋上,這樣顯得仍不夠 所以以前的這套結論就來用作迅速判別閉包,而本文就來記錄、總結閉包的完整理解, **記住: 並不是以前的結論不對,而是不夠深入** 先總結出閉包最終的完整理解,再不斷的對其進行解釋 所謂閉包,就是: - 當函式不僅能夠在定義時所在的詞法作用域以外進行呼叫 - 呼叫時還能正常訪問定義時的詞法作用域 詞法作用域看著很眼熟,不明白什麼意思? 沒關係 ## 詞法作用域 JavaScript中作用域就是指: ==用來管理引擎是如何在當前作用域或者巢狀的父子作用域中根據識別符號進行變數和變數值查詢的一套規則== 一提到作用域,我們大多數可以能就會想到: 哦! 全域性作用域、函式作用域、變數查詢與作用域鏈,甚至塊作用域。但是我想提前說的是: 詞法作用域這些概念息息相關,它屬於是這些概念的集合。一個更加上層的概念 作用域有兩種模型: 詞法作用域和動態作用域 JavaScript中主要的是詞法作用域,但是也有動態作用域的身影,由於本節主要介紹的是詞法作用域,所以不會涉及動態作用域 之所以將作用域命名為詞法作用域,是因為這個概念和JavaScript工作流程中的編譯流程息息相關。雖然JavaScript作為一門動態語言不需要像java和C等語言那樣需要在執行前手動編譯,但不代表它沒有這一套過程 編譯階段最重要的一個工作就是詞法化(單詞化), 它對原始碼進行分析,然後賦予單詞和程式碼塊含義,作用域是在詞法階段定義的(在編寫程式碼階段),因此當編譯器進行詞法分析時就會保持作用域不變(至少大部分情況是這樣的) 既然作用域與詞法化關聯這麼深,所以就命名為詞法作用域了 總結來說: 詞法作用域是指作用域由程式碼書寫時各種變數和函式的位置所決定,編譯的詞法階段通過作用域大概預測出執行過程中如何進行查詢 ## 回到閉包 對詞法作用域做了一大段枯燥的解釋後,終於可以回到閉包了。閉包就是基於詞法作用域書寫程式碼時所產生的自然結果,它並不是一種新的語法或技術。 ==當函式能夠記住並訪問所在的詞法作用域時,閉包就產生了,不管函式是否在當前詞法作用域之外執行== 這時候回到上面對閉包完整總結那細細品一品先吧 ``` js // 一個最典型的閉包 function foo() { var a = 2 function bar() { console.log(a) } return bar } var baz = foo() baz() // 2 ``` 根據詞法作用域的概念,更細來說是作用域。 bar函式擁有一個覆蓋foo函式的作用域,在當前來說bar的作用域範圍是最為之廣闊的,bar能夠訪問foo的作用域甚至全域性作用域,用圖來表達一下我說的意思 ![](https://img2020.cnblogs.com/blog/2117688/202103/2117688-20210317174302120-390687328.png) foo執行後將返回值賦值給全域性變數baz,呼叫baz實際上是根據引用傳遞去呼叫內部的這個bar而已。根據JS的垃圾回收機制在foo執行完畢後其內部所有的變數和函式都會被回收,但是由於回收機制不會對仍有引用的物件進行回收,所以bar作用域仍然存在。 ==在回收機制和詞法作用域是靜態確定的相互作用下,baz不僅能夠正常呼叫,還能夠正常訪問baz引用著的那個函式(bar函式)在定義時的詞法作用域== 於是乎閉包就這樣產生了,再回到前面一開始我給出的閉包的完整理解,大家應該就明白是怎麼個意思了 我對閉包概念的再一次深入理解,對閉包概念的深入總結到這就結束了 那閉包有什麼用呢? 書中作者的經驗是,閉包最大的作用就是對當今JavaScript模組化規範的貢獻。模組化正是利用閉包發揮出了強大的威力,對於JavaScript模組化可以看看我的[這篇學習筆記](https://www.cnblogs.com/fitzlovecode/p/learn_jsModular.html) ## 利用閉包編寫模組 模組擁有兩個必要條件: 1. 外部必須是一個函式,且函式必須至少被呼叫一次(每次呼叫產生的閉包作為新的模組例項) 2. 外部函式內部至少有一個內部函式, 內部函式用於修改和訪問各種內部私有成員 利用閉包編寫一個模組(例子) ``` js 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建立,這種方式我們成為單例模式 ``` js 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規範的語法是大概這樣的 暴露模組 ``` js // 暴露沒有依賴的模組 define(function () { // do something return 模組 }) // 暴露有依賴的模組 define( ['依賴1','依賴2'], function (m1, m2) { // do something return 模組 } ) ``` 引入模組 ``` js requirejs( ['依賴'], function (m1) { // do something } ) ``` 我們模擬實現這些功能 ``` js // 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()) }) // =============================測試========================== ``` ## 寫在最後 文章到這就結束了, 模擬實現只能對最簡單的功能進行模擬 文章看起來有點囉嗦(尤其是前面),寫的不好的地方希望大家見諒