JS高阶六(JS模块化上)
Posted 稻香Snowy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS高阶六(JS模块化上)相关的知识,希望对你有一定的参考价值。
以功能块为单位进行程序设计,实现其求解算法的方法称为模块化,原则是“高内聚,低耦合”。“高内聚”尽量减少不同文件中函数的交叉引用,“低耦合”是模块与模块之间要相互独立。模块的目的是为了降低程序复杂度,使程序设计,调试和维护等操作简单化。
一、为什么要使用模块
模块化可以使你的代码低耦合,功能模块直接不互相影响。我个人认为模块化主要有以下几点好处:
1.可维护性:根据定义,每个模块都是独立的。良好设计的模块会尽量与外部的代码撇清关系,以便于独立对其进行改进和维护。维护一个独立的模块比起一团凌乱的代码来说要轻松很多。
2.命名空间:在javascript中,最高级别的函数外定义的变量都是全局变量(这意味着所有人都可以访问到它们)。也正因如此,当一些无关的代码碰巧使用到同名变量的时候,我们就会遇到“命名空间污染”的问题。这样的问题在我们开发过程中是要极力避免的。(依然管理,函数命名冲突,依赖关系处理等)。
3.可复用性:现实来讲,在日常工作中我们经常会复制自己之前写过的代码到新的项目中。
复制粘贴很方便,但是难道我们找不到更好的方法了吗?要是有一个可以重复利用的模块不是更好吗?
二、如何引入模块
引入模块有很多种方式,这里我们先介绍一些:
模块模式
模块模式一般用来模拟类的概念(因为原生javascript并不支持,虽然最新的ES6里引入了Class不过还不普及)这样我们就能把公有和私有方法还有变量存储在一个对象中——这就和我们在java或Python里使用类的感觉一样了。这样我们就能在公开调用API的同时,仍然在一个闭包范围内封装私有变量和方法。
实现模块模式的方法有很多种,下面的例子是通过匿名闭包函数的方法。(在Javascript中函数是创建作用域的唯一方式)。
例1:匿名闭包函数(立即执行函数)
var global='Hello, I am a global variable';
(function(){
//在函数的作用域中下面的函数是私有的
var myGrades=[93,95,88,0,55,91];
var average=function(){
var total=myGrades.reduce(function(accumulator,item){
return accumulator+item
},0);
return 'Your average grade is ' + total / myGrades.length + '.';
}
var failing = function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;});
return 'You failed ' + failingGrades.length + ' times.';
}
console.log(failing());
console.log(global);
}());
通过这种构造,我们的匿名函数有了自己的作用域或“闭包”。这允许我们从父(全局)命名空间隐藏变量。
这种方法的好处在于,你可以在函数内部使用局部变量,而不回意外覆盖同名全局变量,但仍然能够访问到全局变量。如果不使用一些模块化的框架,而只用jQuery或者Zepto类库完成开发,匿名函数实现模块化也是常用的方式。有兴趣的朋友可以去阅读jQuery源码,里面大量的采用了这种方式。
这种方法的好处在于,你可以在函数内部使用局部变量,而不会意外覆盖同名全局变量,但仍然能够访问到全局变量。
例2:全局引入
另一种比较受欢迎的方法是一些诸如jQuery的库使用全局引入。和我们刚刚才举例的匿名闭包函数很相似,只是传入全局变量的方法不同:
(function(globalVariable){
//在函数的作用域中下面的变量是私有的
var privateFunction=function(){
console.log('Shhhh, this is private!');
}
//通过全局变量设置下列方法的外部访问接口
//与此同时这些方法又在函数内部
globalVariable.each=function(collection,iterator){
if(Array.isArray(collection)){
for(var i=0;i<collection.length;i++){
iterator(collection[i],i,collection);
}
}else{
for(var key in collection){
iterator(collection[key],key,collection);
}
}
};
globalVariable.filter = function(collection, test) {
var filtered = [];
globalVariable.each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
};
globalVariable.map = function(collection, iterator) {
var mapped = [];
globalUtils.each(collection, function(value, key, collection) {
mapped.push(iterator(value));
});
return mapped;
};
globalVariable.reduce = function(collection, iterator, accumulator) {
var startingValueMissing = accumulator === undefined;
globalVariable.each(collection, function(item) {
if(startingValueMissing) {
accumulator = item;
startingValueMissing = false;
} else {
accumulator = iterator(accumulator, item);
}
});
return accumulator;
};
}(globalVariable));
这个例子中,globalVariable 是唯一的全局变量。这种方法的好处是可以预先声明好全局变量,让你的代码更加清晰可读。
例3:对象接口
像下面这样,还有一种创建模块的方法是使用独立的对象接口:
var myGradesCalculate=(function(){
//在函数的作用域中下面的变量是私有的
var myGrades=[93,95,88,0,55,91];
//通过接口在外部访问下列方法
//与此同时这些方法又都在函数内部
return {
average:function(){
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'Your average grade is ' + total / myGrades.length + '.';
},
failing: function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return 'You failed ' + failingGrades.length + ' times.';
}
}
})();
myGradesCalculate.failing();//You failed 2 times.
myGradesCalculate.failing();//Your average grade is 70.33333333333333.
例4:揭示模块模式
这和我们之前的实现方法非常相近,除了它会确保,在所有的变量和方法暴露之前都会保持私有:
var myGradesCalculate=(function(){
//在函数的作用域中,下面的变量是私有的
var myGrades=[93, 95, 88, 0, 55, 91];
var average=function(){
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'Your average grade is ' + total / myGrades.length + '.';
};
var failing=function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return 'You failed ' + failingGrades.length + ' times.';
};
//将指针指向私有方法
return {
average:average,
failing:failing
}
})();
myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
到这里,其实我们只聊了模块模式的冰山一角。感兴趣的朋友可以阅读更详细的资料。
深入理解javascript模块模式
深入理解javascript系列(3):全面解析Module模式
CommonJS和AMD
上述的所有解决方案都有一个共同点:使用单个全局变量来把所有的代码包含在一个函数内。由此来创建私有的命名空间和闭包作用域。
虽然每种方法都比较有效,但也都有各自的短板。
有一点,作为开发者,你必须清楚地了解引入依赖文件的正确顺序。就拿Backbone.js来举个例子,想要使用Backbone就必须在你的页面里引入Backbone的源文件。
然而Backbone又依赖Underscore.js所以Backbone的引入必须在其之后。
而在工作中,这些依赖管理经常会成为让人头疼的问题。
另外一点,这些方法也有可能会引起命名空间冲突。举个例子,要是你碰巧写了来两个重名的模块怎么办?或者你需要一个模块的两个版本怎么办?
难道就没有不通过全局作用域来实现的模块方法吗?
当然是有的,接下来介绍两个广受欢迎的解决方案:CommonJS和AMD
CommonJS
CommonJS扩展了JavaScript声明模块的API。
CommonJS模块可以很方便的将某个对象导出,让它们能够被其他模块通过require语句来引入。要是你写过Node.js应该很熟悉这些语法。
通过CommonJS,每个JS文件独立地存储它模块的内容(就像一个被括起来的闭包一样)。在这种作用域中,我们通过module.exports语句来导出对象为模块,再通过require语句来引入。
示例:
function myModule(){
this.hello=function(){
return 'hello!'
}
this.goodbye=function(){
return 'goodbye!';
}
}
module.exports=myModule;
通过指定导出的对象名称,CommonJS模块系统可以识别在其他文件引入这个模块时应该如何解释。
然后在某文件中想要调用myMoudle的时候,只需要require一下:
var myModule=require('myMoudle');
var myModuleInstance=new myModule();
myModuleInstance.hello();//hello
myModuleInstance.goodbye();//goodbye
这种实现比起模块模式有两点好处:
避免全局命令空间污染
明确代码之间的依赖关系
需要注意一点的是,CommonJS以服务器优先的方式来同步载入模块,假设我们引入三个模块的话,它们会一个个的被载入。
它在服务器端用起来很好,可是在浏览器里就不会那么高效了。毕竟读取网络的文件要比本地耗时更长。只哎哟它还在读取模块,浏览器载入的页面就会一直卡着不动(在下一篇我们会讨论如何解决这个问题)
AMD
CommonJS已经挺不错了,但是假如我们想要实现异步加载模块该怎么办?答案是Aynchronous Module Definition(异步模块定义规范),简称AMD。
通过AMD载入模块的代码一般这么写:
define(['myModule','myOtherModule'],function(myModule,myOtherModule){
console.log(myModule.hello());
});
这里我们使用define方法,第一个参数是依赖的模块,这些模块都会在后台无阻塞的加载第二个参数作为加载完毕的回调函数。
回调函数将会使用载入的模块作为参数。在这个例子中就是myModule和myOtherModule。最后,这些模块本身也需要通过define关键词定义。
拿myMoudle来举个例子:
define([],function(){
return {
hello:function(){
console.log('hello');
},
goodbye:function(){
console.log('goodbye');
}
}
});
重申一下,不像CommonJS,AMD是优先浏览器的一种异步载入模块的解决方案(记得,很多人认为一个个载入小文件是很低效的,我们将在下一篇中介绍如何打包模块)。
除了异步加载以外,AMD的另一个优点是你可以在模块里使用对象,函数,构造函数,字符串,JSON或者别的数据类型,而CommonJS只支持对象。
再补充一点,AMD不支持Node里的一些诸如IO,文件系统等其他服务器端的功能。另外语法上写起来也比CommonJS麻烦一些。
UMD
在一些同时需要AMD和CommonJS功能的项目中,你需要使用另一种规范:Universal Module Definition(通用模块定义规范)。
UMD创造了一种同时使用两种规范的方法,并且也支持全局变量的定义。所以UMD的模块可以同时在客户端和服务器端使用。
下面是一个解释其功能的例子:
(function(root,factory){
if(typepof define==='function' && define.amd){
//AMD
define(['myModule','myOtherModule'],factory);
}else if(typeof exports==='object'){
//CommonJS
module.exports=factory(require('myModule'),require('myOtherMoule'));
}else{
// Browser globals (Note: root is window)
root.returnExports=factory(root.myModule,root.myOtherModule);
}
}(this,function(myModule,myOtherModule){
//Methods
function notHelloOrGoodbye(){};// A private method
function hello(){}; // A public method because it's returned (see below)
function goodbye(){}; // A public method because it's returned (see below)
return {
hello:hello,
goobye:goobye
}
}));
更多有关UMD的例子情看其github上的官方repo
原生JS
我们最后再定义一种定义模块的方式
你可能已经注意到了,上述的这几种方法都不是JS原生支持的。要么是通过模块模式来模拟,要么是使用CommonJS或AMD。
幸运的是在JS的最新规范ECMAScript6中,引入了模块的功能。
ES6的模块功能汲取了CommonJS和AMD的优点,拥有简洁的语法并支持异步加载,并且还有其他诸多更好的支持。
我最喜欢的ES6模块功能的特性是,导入是实时只读的。(CommonJS只是相当于把导出的代码复制过来)。
来看例子:
//lib/counter.js
var counter=1;
function increment(){
counter++;
}
function decreament(){
counter--;
}
module.exports={
counter:counter,
increment:increment,
decrement:decrement
};
//src/main.js
var counter=require('../.../lib/counter');
counter.increment();
console.log(counter.counter);//1
上面的例子中,我们一共创建了两份模块的实例,一个在导出的时候,一个在引入的时候。
在main.js当中的实例和原本模块完全不相干的。这也就解释了我为什么调用了counter.increment()之后仍然返回1.因为我们引入的counter变量和模块里的是两个不同的实例。
所以调用counter.increment()方法只会改变模块中的counter。想要修改引入的counter只有手动一下啦:
counter.counter++;
console.log(counter.counter);//2
而通过import语句,刻印引入实时只读的模块:
//lib/counter.js
export let counter=1;
export function increment(){
counter++;
}
export function decrement(){
counter--;
}
//src/main.js
import* as counter from '../../counter';
console.log(counter.counter);//1
counter.increment();
console.log(counter.counter);//2
这看起来很酷不是么?这样就实现了我们把模块分隔在不同的文件里,需要的时候又可以合并在一起而且不影响它的功能。
原文链接
以上是关于JS高阶六(JS模块化上)的主要内容,如果未能解决你的问题,请参考以下文章