前两天有朋友拿了这样一段代码来问我,“我想把一段代码写成模块化的样子,你帮我看看是不是这样的。”,代码大概是这样的:
(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
上了吗,他似懂非懂,我只好说,你可能需要回头去看看AMD
和CMD
规范了,我大概能够理解这其中的缘由,毕竟这段时间前端发展的速度飞快,加上webpack
也不需要自己配,包括vue
,react
,angular
在内的框架类库都有一键生成项目的工具,从而只需要使用下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
}
}
以上代码似乎解决了全局变量的问题,但是其中的
a
和b
两个变量还是可能被修改,其中包含的进化有限。
-
立即执行函数
回过头来看文章开头的代码,姑且不论以上代码的错误之处,稍作修改,这算是最初的一种关于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
}
})()
这就是一种最简单的模块化的方式,利用闭包的特性,设置了两个只有在被暴露出来的
add
和reduce
方法内部才能访问到的两个变量,从而保证了函数的局部变量不可被修改的特性,这次的进化用到了闭包,从而实现了部分有效的目的。
-
放大模式
其实开头的代码更符合另外一种叫做放大模式的方法,不过一般来说不会讲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.js
和b.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
的时代,我们一般会使用jQuery
,underscore
这类的类库,如果按照往常的样子我们会将代码写成下面这副模样:
<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
规范到底解决了什么样的问题呢,主要有几点:
AMD
是“异步模块定义”的缩写,也就是说,其中内容是异步加载的,从而让页面不被js
的加载阻塞,最大程度上的避免了页面假死等情况的产生。AMD
的一个好处在与依赖前置,所有被使用到的模块都会被提前加载好,从而加快运行速度。
那么,commonjs
规范和AMD
规范有什么区别呢
- 运行环境不同,
commonjs
规范只能运行在node
端,而AMD
规范则被用到浏览器端- 由于运行环境的不同,二者的加载机制也不同,
commonjs
中的require
是同步执行的,而AMD
中则是异步的。
-
ES2015
模块化
在ES2015
中,可以使用export
, export default
, import
import * as
等操作符来作模块化的功能,但是这个规范现在尚未被任何浏览器加入规范中,我目前的Chrome
版本为63.0.3239.132
,也无法原生支持,不过现阶段我们几乎都用上了这个规范,这一切都只能归功于babel
,webpack
和rollup
等新工具的出现,既然如此,那就拥抱未来吧,不过有一点,需要在了解原理的前提下,不然,倘若有一天,真的需要我们来封装一个小小的模块的时候,没有了那些工具,我们该从何下手呢。