模块是闭包最强大的一种应用,在团队开发中是迫切需要的,可以更好的划分团队人员职能,做到精细化管理而互不干扰。
模块化的发展
初学者的代码 - 原始写法
作为一名初学者,写代码可能是一堆一堆的,所以为了好看一点,就把这一段代码包含在一个函数里面以供调用。
1 |
function m1(){ |
上面的函数m1()和m2(),组成一个模块。使用的时候,直接调用就行了。
缺点是污染了全局变量,容易照成变量名冲突,各模块直接关系混乱。
如果代码都是大段大段的,最后导致写成了一堆无人能看懂的代码。
高级一点的代码 - 对象写法
所以就有人使用对象的写法
1 |
var module1 = { |
上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。
但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。
1 |
module1._count = 5; |
初具样子的代码 - 闭包写法
所以闭包来了,可以做到变量的私有化
1 |
function CoolModule() { |
使用上面的写法,外部代码无法读取内部的something、another变量。
首先,CoolModule() 只是一个函数,但它 必须被调用 才能成为一个被创建的‘模块实例’(我们暂且叫foo为‘模块实例’或者‘实例’)。
第二,CoolModule() 函数返回一个对象。这个返回的对象拥有指向内部函数的引用,但是 没有 指向内部数据变量的引用;也可以看做是模块的公有API。我们也可以仅仅返回一个内部函数,jQuery 就是一个很好地例子。
这个返回值对象最终被赋值给外部变量 foo,然后我们可以在这个API上访问那些属性,比如 foo.doSomething()。
doSomething() 和 doAnother() 函数拥有模块“实例”内部作用域的闭包,也就是说foo拥有了访问模块词法作用域的能力。
这种方式可以可以重复创建多个‘实例’,如果想仅仅创建一个‘实例’,就要使用IIFE。
再次升级 - IIFE写法
所以立即调用函数表达式来了,做到了在代码加载成功之后自动创建一个‘实例’,并且不会再次创建一个新的‘实例’;因为没有同样的‘实例’,所以这个‘实例’是‘单例’的。
1 |
var foo = (function CoolModule() { |
模块的继承
如果一个模块很大要分成几个部分,或者要继承另一个模块,这是就要采用下面这种方式,同时最后一行的module1 || {}是必须的,防止浏览器异步加载导致的引用错误。
而且这样的好处是和外界完全隔离,也不需要在作用域链上进行疯狂的查找。
1 |
var module1 = (function (mod){ |
模块间的相互引用
模块间的相互引用也是没有问题的,只是写法有点特殊。
1 |
// 1. 首先注册模块实例 |
这里要注意的是要先注册模块实例,然后再实现各模块实例,最后要在全部模块加载完代码之后再执行即可。
ES6模块
ES6 引入了模块化,其设计思想是在编译时就能确定模块的依赖关系,以及输入和输出的变量。ES6 模块必须被定义在一个分离的文件中(每个模块一个)。浏览器/引擎拥有一个默认的“模块加载器”,它在模块被导入时同步地加载模块文件。
ES6 的模块化分为导出(export) @与导入(import)两个模块。
ES6 的模块自动开启严格模式,不管你有没有在模块头部加上 use strict;。
模块中可以导入和导出各种类型的变量,如函数,对象,字符串,数字,布尔值,类等。
每个模块都有自己的上下文,每一个模块内声明的变量都是局部变量,不会污染全局作用域。
每一个模块只加载一次(是单例的), 若再去加载同目录下同文件,直接从内存中读取。
应用
bar.js
1 |
function hello(who) { |
foo.js
1 |
// 导入“bar”模块中的`hello()` |
app.js
1
2
3
4
5
6
7
8
9
// 导入`foo`和`bar`整个模块
module foo from "foo";
module bar from "bar";
console.log(
bar.hello( "rhino" )
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO
在浏览器上直接使用ES6模块
module “my-module.js”
1 |
function cube(x) { |
script demo.js
1 |
import { cube } from 'my-module.js'; |
html
1 |
<script type="module" src="./demo.js"></script> |
过渡阶段的模块
在ES6模块未出现之前出现了一些大家公认的模块规范,首先来看一个简单的例子,并了解实现原理。然后再了解其他的模块规范。
目前,通行的js模块规范主要有两种:CommonJS和AMD。
实现原理
模块依赖加载器/消息机制实质上都是将这种模块定义包装进一个友好的API。
1 |
var MyModules = (function Manager() { |
这段代码的关键部分是 modules[name] = impl.apply(impl, deps)。这为一个模块调用了它的定义的包装函数(传入所有依赖),并将返回值,也就是模块的API,存储到一个用名称追踪的内部模块列表中。
这里是我可能如何使用它来定义一个模块:
1 |
MyModules.define( "bar", [], function(){ |
模块“foo”和“bar”都使用一个返回公有API的函数来定义。“foo”甚至接收一个“bar”的实例作为依赖参数,并且可以因此使用它。
它们只是满足了模块模式的两个性质:调用一个函数定义包装器,并将它的返回值作为这个模块的API保存下来。
Commonjs
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。
这标志”Javascript模块化编程”正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。
node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。
1 |
var math = require('math'); |
然后,就可以调用模块提供的方法:
1 |
var math = require('math'); |
因为这个系列主要针对浏览器编程,不涉及node.js,所以对CommonJS就不多做介绍了。我们在这里只要知道,require()用于加载模块就行了。
AMD
CommonJS规范不适用于浏览器环境,如果在浏览器中运行,会有一个很大的问题。
服务器端因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态,导致后面的代码不能继续执行。
因此采用”异步加载”(asynchronous),这就是AMD规范诞生的背景。
AMD)是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
define() 函数
本规范只定义了一个函数 “define”,它是全局变量。函数的描述为:
1 |
define(id?, dependencies?, factory); |
- 第一个参数,id - 名字:
是个字符串。它指的是定义中模块的名字,这个参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。 - 第二个参数,dependencies - 依赖:
是个定义中模块所依赖模块的数组。依赖模块必须根据模块的工厂方法优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。
本规范定义了三种特殊的依赖关键字。如果”require”,”exports”, 或 “module”出现在依赖列表中,参数应该按照CommonJS模块规范自由变量去解析。 - 第三个参数,factory - 工厂方法:
为模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。
define.amd 属性
为了清晰的标识全局函数(为浏览器加载script必须的)遵从AMD编程接口,任何全局函数应该有一个”amd”的属性,它的值为一个对象。这样可以防止与现有的定义了define函数但不遵从AMD编程接口的代码相冲突。
举例说明
创建一个名为”alpha”的模块,使用了require,exports,和名为”beta”的模块:
1
2
3
4
5
6
7
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
exports.verb = function() {
return beta.verb();
//Or:
return require("beta").verb();
}
});
一个使用了简单CommonJS转换的模块定义:
1
2
3
4
5
6
define(function (require, exports, module) {
var a = require('a'),
b = require('b');
exports.action = function () {};
});
RequireJS
有人说 AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出,这一点没有深入的探究是否正确,但是根据AMD出现的原因,个人感觉是正确的。
RequireJS 是实现AMD规范的一个非常流行的库。使用前先去 官方网站下载 最新版本。
1 |
<!-- async 属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上 --> |
主模块写法
1 |
require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){ |
or
1
2
3
define(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){
// some code here
});
子模块的写法也是一样的。
自定义模块加载行为
自定义模块加载行为通过 require.config() 实现,写在主模块的头部。
1 |
require.config({ |
require.js要求,每个模块是一个单独的js文件。这样的话,如果加载多个模块,就会发出多次HTTP请求,会影响网页的加载速度。因此,require.js提供了一个优化工具,当模块部署完毕以后,可以用这个工具将多个模块合并在一个文件中,减少HTTP请求数。
插件
RequireJS 插件 实现了一些特定的功能。
domready插件,可以让回调函数在页面DOM结构加载完成后再运行。
1
2
3
require(['domready!'], function (doc){
// called once the DOM is ready
});
text和image插件,则是允许require.js加载文本和图片文件。
1 |
define([ |
类似的插件还有json和mdown,用于加载json文件和markdown文件。
CMD
区别于 AMD 的异步加载,CMD 规范是懒加载,在用的时候才加载,一个文件是一个模块
defined() 函数
模块定义使用 define 关键字的方法,并接收一个factory的方法
1 |
define(function(require, exports, module) { |
- define函数接受单个参数模块工厂。
- factory可以是一个函数或其他有效的值。
- 如果factory是函数,则该函数的前三个参数(如果已指定)必须依次为
requireexports和module。 - 如果factory不是函数,则将模块的导出设置为该对象。
具体的用法可以查看 Common Module Definition / draft CMD 模块定义规范
seajs
seajs 使用的是CMD模块规范,具体的使用方法请查看文档
到底哪个好用
RequireJs 和 seajs 有什么区别,哪一个更好用,这个在现在已经9012了,很多人都在用ES6的模块,还讨论这个话题就是为了六个念想。
前端为了减少请求次数都会打包到一个文件中,所以这两个就没什么区别。
谁说不是呢。
鉴于 RequireJs 应用更广泛,工程化更好,解决方案更多,推荐使用 RequireJs 。
在下方参考中也有很多的讨论,可自行查看
参考
你不懂JS:作用域与闭包
Javascript模块化编程(一):模块的写法
Javascript模块化编程(二):AMD规范
Javascript模块化编程(三):require.js的用法
export
import
详解JavaScript模块化开发
CommonJS
AMD (中文版))
require (中文版))
RequireJS
config-shim
【requireJS源码学习02】data-main加载的实现
RequireJS 插件
CMD 模块定义规范 –感觉说地不对,最好看接下来的英文版
CMD
seajs
与 RequireJS 的异同
SeaJS与RequireJS最大的区别
LABjs、RequireJS、SeaJS 哪个最好用?为什么?
AMD 和 CMD 的区别有哪些?
