JavaScript 模块化编程
前言
- 模块是任何大型应用程序架构中不可缺少的一部分,可以使我们清晰地分离和组织项目中的代码单元
- 通过移除依赖,松耦合可以使应用程序的可维护性更强
- 在 JavaScript 中,并没有提供原生的、有组织性的引入模块方式
- 下面列出目前常见的集中模块化解决方案
- 对象字面量表示法
- Module 模式
- AMD 模块
- CommonJs 模块
- ES Harmony 模块
对象字面量表示法
- 对象字面量可以认为是包含一组键值对的对象,每一对键和值由冒号分隔
对象字面量不需要使用 new 运算符进行实例化,在对象的外部也可以给对象添加属性和方法
var myModule = { property: "amanisky", // 对象字面量可以包含属性和方法 // 例如,可以声明模块的配置对象 config: { useCaching: true, language: "en" }, // 基本方法 method1: function () { console.log("method1"); }, // 根据当前配置输出信息 method2: function () { console.log("Caching is:" + '(this.config.useCaching) ? "enabled" : "disabled"'); }, // 修改配置 method3: function (newConfig) { if (typeof newConfig === "object") { this.config = newConfig; } } }
- 如上所示
- 使用对象字面量有助于封装和组织代码,然后不同的对象字面量模块再构成复杂的项目
Module 模式
- Module 模式最初定义在传统的软件工程中,为类提供私有和公有封装的方法
- 在 JavaScript 中,并不能直接声明类,因此使用闭包来封装私有属性和方法,进而模拟类的概念,从而在 JavaScript 中实现 Module 模式
通过这种方式,就使得一个单独的对象拥有公有/私有方法和变量,从而屏蔽来自全局作用域的特殊部分,大大降低了变量声明和函数声明之间冲突的可能性
var myModule = (function () { // 私有变量 var privateVar = 0; // 私有函数 var privateFun = function (foo) { console.log(foo); }; return { // 公有变量 publicVar: "foo", // 公有函数 publicFun: function (arg) { // 修改私有变量 privateVar++; // 调用私有方法 privateFun(arg); } }; })();
- 如上所示
- 通过使用闭包我们封装了私有变量和方法,只暴露一个接口供其他部分调用
- 私有变量(privateVar)和私有方法(privateFun)被局限于模块的闭包之中,只能通过公有方法才能访问
- 该模式返回一个对象,可以为返回的对象添加属性和方法供外部调用者使用
- 在对象的外部也可以给对象添加属性和方法
- Module 模式非常简洁,但也有缺点和劣势
- 当想改变可见性时,需要修改每一个曾经使用该成员的地方,不利于维护和升级,耦合度不理想
- 在之后新添加的方法里,并不能访问以前声明的私有方法和变量
- 因为闭包只在创建时完成绑定
- 无法为私有方法创建自动化单元测试,
- 修改私有方法及其困难,需要复写所有与私有方法交互的公有方法,bug 修正时工作量会很大
- 不能轻易的扩展私有方法
脚本加载器
AMD 模块
- AMD 全称是 Asynchronous Module Definition,即异步模块加载机制
- 它诞生于使用 XHR+eval 的 Dojo 开发经验,其整体目标是提供模块化的 JavaScript 解决方案,避免未来的任何解决方案收到过去解决方案缺点的影响
- AMD 模块格式本身就是对定义模块的建议,其模块和依赖都可以进行异步加载,具有高度的灵活性,清除了代码和模块之间可能惯有的紧耦合
- AMD 有两个非常重要的方法
- define(module-name? array-of-dependencies? module-factory-or-object)
- 用于模块定义
- module-name 模块标识字符串,可选参数;如果没有这个属性,则称为匿名模块
- array-of-dependencies 是一个数组,表示所依赖的模块,可选参数
- module-factory-or-object 模块的实现或一个 JavaScript 对象
- require(array-of-dependencies, callback)
- 用于加载 JavaScript 文件或模块的代码,获取依赖
- array-of-dependencies 是一个数组,表示所依赖的模块
- callback 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用;加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块
- 动态加载依赖的示例
- AMD 模块可以使用插件,当我们加载依赖时,可以加载任意格式的文件
- 使用 AMD 模块编写模块化的 JavaScript 代码,比现有的全局命名空间和 script 标签解决方案更加简介,没有全局命名空间污染,在需要的时候可以延迟加载脚本
- define(module-name? array-of-dependencies? module-factory-or-object)
/**
* 定义 myModule 模块
* 该模块依赖 foo、bar
* foo、bar 作为参数映射到函数上
*/
define("myModule", ["foo", "bar"], function (foo, bar) {
// 创建模块
var myModule = {
myFun: function () {
console.log("Hello");
}
}
// 返回定义的模块
return myModule;
}
);
/**
* require.js 会先加载 jQuery、underscore,然后再运行回调函数。
*/
require(['jquery', 'underscore'], function ($, _) {
// some code here
});
define(function (requrie) {
var isReady = false, foobar;
requrie(["foo", "bar"], function (foo, bar) {
isReady = true,
foobar = foo() + bar();
});
// 返回定义的模块
return {
isReady: isReady,
foobar: foobar
};
}
);
CommonJS 模块
- CommonJS 规范建议指定一个简单的 API 来声明在浏览器外部工作的模块
- 与 AMD 不同,它试图包含更广泛的引人关注的问题,如:IO、文件系统等
- 从结构来看,CommonJS 模块是 JavaScript 中可以复用的部分,导出特定对象,以便可以用于任何依赖代码
- CommonJS 有两个非常重要的方法
- exports
- exports 包含了一个模块希望其他模块能够使用的对象
- require
- require 函数用来导入其他模块的导出,即用来加载其他模块依赖
- exports
- 浏览器端可以使用 CommonJS 组织模块,但不少开发者认为 CommonJS 更适合于服务器端开发,因为 CommonJS API 具有面向服务器的特性,如:io、sysytem 等
- NodeJS 使用的就是 CommonJS 规范
- 当一个模块可能用于服务端时,开发人员倾向于选择 CommonJS,其他情况下使用 AMD
// 新定义的模块方法
function log(arg) {
console.log(arg);
}
// 把方法暴露给其他模块
exports.log = log;
// ./lib是需要的一个依赖
var lib = requrie("./lib");
// 新定义的模块方法
function foo() {
lib.log("jeri");
}
// 把方法暴露给其他模块
exports.foo = foo;
ES6 Module
- ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量
- ES6 可以在编译时就完成模块加载(静态加载),效率要比 CommonJS 模块的加载方式高
- 模块功能主要由两个命令构成:export 和 import
- export 命令用于规定模块的对外接口;可以使用 as 关键字重命名
- import 命令用于输入其他模块提供的功能;可以使用 as 关键字重命名
// CommonJS模块
let { stat, exists, readFile } = require('fs');
/**
* 上面代码的实质是整体加载 fs 模块(即加载 fs 的所有方法),生成一个对象(_fs),然后再从这个对象上读取 3 个方法。
* 这种加载称为"运行时加载";因为只有运行时才能得到这个对象。
*/
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
// ES6模块
import { stat, exists, readFile } from 'fs';
/**
* 上面代码的实质是从 fs 模块加载 3 个方法,其他方法不加载。
* 这种加载称为"编译时加载"或"静态加载",即 ES6 可以在编译时就完成模块加载,效率比 CommonJS 高。
* 当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
*/