JavaScript模块发展变迁史

137 阅读6分钟

前两天有朋友拿了这样一段代码来问我,“我想把一段代码写成模块化的样子,你帮我看看是不是这样的。”,代码大概是这样的:

(function(global) {
    var myModules = {
        name: 'xxx',
        location: 'chengdu',
        intro: function() {
                return `his name is ${myModules.name} and come from ${myModules.location}`
            },
        }
        // some other code...
        if(typeof module === 'undefined')
            global.myModules = myModules
        else
            module.exports = myModules
})(this)

“可是我这段代码在全局还是能myModules这个属性啊?” 我一脸懵,还有这种操作,为什么你的立即执行函数要把this传进去呢,这样不久将里面的内容挂在到了window上了吗,他似懂非懂,我只好说,你可能需要回头去看看AMDCMD规范了,我大概能够理解这其中的缘由,毕竟这段时间前端发展的速度飞快,加上webpack也不需要自己配,包括vuereactangular在内的框架类库都有一键生成项目的工具,从而只需要使用下import * from '*'export default {},而这种便利会让新人不再需要去学习基本原理就能快速上手,毕竟现在都ES2018了呀。

  • 对象形式

最开始的时候,为了减少全局变量,一般会使用一个对象的方式来对所有的变量方法进行一个包装:

    var obj = {
        a: 1,
        b: 2,
        sum: function(val) {
           return obj.a + obj.b + val
        },
        rest: function(val) {
           return val - obj.a - obj.b
        }
   }

以上代码似乎解决了全局变量的问题,但是其中的ab两个变量还是可能被修改,其中包含的进化有限。

  • 立即执行函数

回过头来看文章开头的代码,姑且不论以上代码的错误之处,稍作修改,这算是最初的一种关于JavaScript模块化的开端,立即执行函数:

var add = (function(){
    var a = 1
    var b = 2
    function sum(count){
        return a + b + count
    }
    function rest(count){
        return count - a - b
    }
    return {
        rest: rest,
        sum: sum
    }
})()

这就是一种最简单的模块化的方式,利用闭包的特性,设置了两个只有在被暴露出来的addreduce方法内部才能访问到的两个变量,从而保证了函数的局部变量不可被修改的特性,这次的进化用到了闭包,从而实现了部分有效的目的。

  • 放大模式

其实开头的代码更符合另外一种叫做放大模式的方法,不过一般来说不会讲window作为放大模式中被传入的对象

    var globalObject = {
      fn1: function() {
        // todo...
      },
      // ...
    }
    globalObject = (function(obj) {
      var name = 'xxx'
      var location = 'xxx'
      function sum(val) { /* todo... */ }
      function rest(val) { /* todo... */ }
      obj.sum = sum
      obj.rest = rest
      return obj
    })(globalObject)

以这种形式来写,可以让全局变量尽量的减少,同时让一个立即执行函数中的代码尽量做到精简。

  • 但是这种放大模式也存在着问题,比如当globalObject内容足够多的时候,很可能会造成命名重复的情况,并且以上所有的方式都不可以减少script标签的数量,所以,我们还是会被模块的加载顺序,命名空间冲突等问题所困扰,这时候,我们应该跨入新时代了。

  • CMD规范

CMD规范来自阿里的框架seajs,当初确实有挺多人使用,不过现阶段已经不再维护了,我也不会,就暂时不说了,只列出来。

  • commonjs

同时,从2009年开始,JavaScript就不再只是一种浏览器端的脚本语言了,nodejs的出现让使用js开发服务端变成了可能,随着node出现的东西还有一个叫做commonjs的规范,在这个规范中,每个文件都是一个模块,有着自己的作用域。

譬如,如下代码

    // 文件a.js
    var a = 1
    // 文件b.js
    console.log(a) // a is not defined.

在这样的特性下,a.jsb.js都有着自己独有的作用域,要在b中对a进行访问,就需要一种加载机制,一般来说,有两种方法能够做到:

方法1

    // 文件a.js
    global.a = 1
    // 文件b.js
    console.log(a) // 1

这种方法挂载在global上,当然是不可取的。

方法2

    // 文件a.js
    exports.a = 1
    // 文件b.js
    var moduleA = require('./a')
    console.log(moduleA.a)
  • AMD规范

requirejs的出现让script标签的减少变成了可能,在requirejs的时代,我们一般会使用jQueryunderscore这类的类库,如果按照往常的样子我们会将代码写成下面这副模样:

<script src="/js/lib/jquery.min.js"></script>
<script src="/js/lib/underscore.min.js"></script>
<script src="/js/app/index.js"></script>
<script src="/js/app/app.js"></script>
<!-- and so on... -->

这样的代码乍一看似乎没什么问题,但是当一个项目的代码量上了一个量级,一切就变得不是这么回事儿了,你会被困在加载顺序,加载时间的问题上,这也就是requirejs能够出现的原因了。

requirejs中,你可以如此改写以上代码:

    // `index.js`
   require(['js/lib/jquery.min', 'js/lib/underscore.min', 'js/app/app'], function($, _, app) {  /*  todo...  */  })
    // `app.js`
   define(['js/lib/jquery.min', 'js/lib/underscore.min'], function($, _) {  /*  todo...  */  })
<script data-main="/index.js" src="/js/require.js"></script>

这里当然显得更加优雅了,在requirejs的推广过程中,AMD规范也就应运而生了,那么,requirejs或者说AMD规范到底解决了什么样的问题呢,主要有几点:

  1. AMD是“异步模块定义”的缩写,也就是说,其中内容是异步加载的,从而让页面不被js的加载阻塞,最大程度上的避免了页面假死等情况的产生。
  2. AMD的一个好处在与依赖前置,所有被使用到的模块都会被提前加载好,从而加快运行速度。

那么,commonjs规范和AMD规范有什么区别呢

  1. 运行环境不同,commonjs规范只能运行在node端,而AMD规范则被用到浏览器端
  2. 由于运行环境的不同,二者的加载机制也不同,commonjs中的require是同步执行的,而AMD中则是异步的。
  • ES2015模块化

ES2015中,可以使用export, export default, import import * as 等操作符来作模块化的功能,但是这个规范现在尚未被任何浏览器加入规范中,我目前的Chrome版本为63.0.3239.132,也无法原生支持,不过现阶段我们几乎都用上了这个规范,这一切都只能归功于babel,webpackrollup等新工具的出现,既然如此,那就拥抱未来吧,不过有一点,需要在了解原理的前提下,不然,倘若有一天,真的需要我们来封装一个小小的模块的时候,没有了那些工具,我们该从何下手呢。