深度解析JavaScript中的模块
概述
有经验的开发者,会把代码拆分成独立的逻辑或者文件,然后再封装成一个个模块。在JS的世界里,模块是一个重要的概念,但由于版本变迁的原因,想要完全掌握着实不易。
我们知道,2009 年发布的ECMASCript 5(简写ES5)标准并没有对模块管理做出规定,导致开发者只能以自己的方式模拟模块管理。于是,就出现了不同的模块管理机制。这期间出现的模块管理方式有如下几种:
- 立即执行函数表达式
- 显示模块
- 异步模块
- 共同模块
- CommonJS
- 通用模块定义
鉴于模块在开发中的重要性,ES6在借鉴Java、Python语言的特性后,引入import、export关键字来管理模块。
1 ES5 模块管理机制
1.1 立即执行函数表达式
使用ES5可以通过立即执行函数来引入第三方JS代码,比如:
(function(){
// 模块的内部逻辑
})();
使用立即执行函数表达式可以达到变量私有化的目的,避免产生全局变量,这种方法的缺点,一是外部环境无法访问模块内部,二是无法形成模块依赖管理机制。
1.2 显示模块声明
为了解决立即执行函数无法将模块暴露给外部的问题,开发者想到了一个办法,就是通过全局对象来实现。
var module = (function(){
function createPerson(name, age){
console.log("name: "+name+", age: "+age);
}
return {
createPerson: createPerson
}
})();
module.createPerson('John', 23);
显示模块声明,将函数返回值返回给一个全局变量。随后就可以通过这个全局变量来访问模块暴露的接口。但是,这种办法还是没有实现模块的管理机制。
1.3 异步模块定义
异步模块定义实现了模块管理规范,比如RequireJS就是按照这种规范实现的第三方库:通过define 函数定义和声明依赖。
define("myModule", ["music"], function(music){
var myModule = {
};
myModule.playMusic = function(){
music.play();
}
return myModule;
}
异步模块定义的好处是所有的模块加载都是异步的,也就是说可以同时加载多个模块。但是,这种方式的缺点是模块不是按照它们声明的样子就行顺序加载。
1.4 共同模块定义
共同模块定义是一种懒加载的模块管理方式,也是通过define函数来定义模块,通常一个文件就是一个模块。
在定义模块的时候,会注入三个参数:require、exports、module。
- require 函数用于动态引用模块
- exports 用来暴露模块接口
- moudule 提供当前模块参数
define("myModule", function(require, exports, module){
var music = require("music");
exports.play = function(){
music.play();
}
});
require(["myModule"], function(myModule){
myModule.play();
});
共同模块定义解决了异步定义加载顺序的问题,但是加载时间低于定义异步方式。但是,开发者可以使用require.async
来实现异步加载。
define("myModule", function(require, exports, module){
var music = require.async("music");
exports.play = function(){
music.play();
}
});
1.5 CommonJS
CommonJS 是Node.js 默认的模块管理规范。他约束一个文件就是一个模块,同时去除了define函数的束缚。在模块中,通过内置的exports对象来导出接口。
var a = require("moduleA");
var b = require("moduleB");
exports.doSomething = function(){
a.doSomething();
b.doSomething();
}
值得一提的是,使用CommonJS 加载模块都是顺序执行的。
1.6 通用模块定义
通用模块定义,从本质上讲就是上述各种模块定义的代理,看下面的代码:
(function(root, factory){
if(typeof define === "function" && define.amd){
// 异步模块定义
define(["moduleA", "moduleB"], factory);
}else if(typeof exports === "object"){
// CommonJS模块定义
module.exports = factory(require("moduleA"), require("moduleB"));
}else{
// 浏览器全局变量
root.returnExports = factory(root.moduleA, root.moduleB);
}
})(this, function(a, b){
return {
pa: a.doSomething(),
pb: b.doSomething()
}
})
这种方式的优点非常明显,不用引入第三方库。只要在每个模块引入即可。但缺点也在于此,会造成代码的冗余。
2 ES6 模块管理机制
ES6 不再从代码层面解决模块的管理机制,而是引入了ES5 中的保留字:import,export。从使用方法上来讲,ES6 的模块管理和CommonJS 颇为类似,也是规定每个文件就是一个模块,模块内部的代码不能从外部访问,必须暴露出来。
import a from "moduleA";
import {
b} from "moduleB";
var func = function(){
a.doSomething();
}
var pa = a.xxx;
export {
func, pa, xxx as pb};
ES6 对模块的引用和CommonJS有着根本的区别,前者不仅可以引用整个模块,也可以引用模块中的变量和函数,这样大大提升了程序开发的灵活性。