模拟实现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())
})

// =============================测试==========================

写在最后

文章到这就结束了, 模拟实现只能对最简单的功能进行模拟

文章看起来有点啰嗦(尤其是前面),写的不好的地方希望大家见谅