WEB开发面试畅谈(1)——Javascript封闭性设计

日常开发中,我们经常会提到模块化开发、代码规范等等,但到了以HTML和Javascript为首的WEB开发这里,一切都变了。

无论是HTML还是Javascript都以其松散的结构和宽松的规范极大降低了开发成本,但又同时引入了新的问题:不同的开发人员有着不同的开发习惯,当然这些习惯也有好有坏,大多数程序员只是坚持着”能运行就可以“的思想观念,而这些并不利于项目的长期发展。于WEB开发中松散的特性,许多思路和经验并不是语言层面约束的,而是通过特有的代码逻辑和结构加以维护。所以在观看下文时,请勿在评论中发表类似如下的评论:”搞那么复杂干嘛?直接var一个变量去改不就好了,干嘛用闭包,我自己写的代码自己清楚,我用人格担保我的调用正确“,扯远了,如果你认同我上面的大体思想,请继续往下看。

大多数开发人员在第一次开发WEB时,一定写过类似如下的代码:

1
2
3
var a = 1;
var b = 2;
alert(a+b);

我们欢呼着把脚步踏入了JS开发行列,并且意识到JS的强大,简单堆叠代码就能实现功能。但时到如今回头看这段代码,确实问题重重。
提到此,我们已经不是关注功能实现的问题,而是设计的问题。功能是个人都能实现,但设计确实良莠不齐因人而异。
以上述代码为例,我们仅仅需要实现alert(a+b)却同时生硬的在全局书写了两个全局变量a、b。而在其余地方这两个变量均无用。
倘若另一段代码也没做封闭处理,也用全局a、b,很容易引发冲突问题,而且不是异常,难于定位。
实现封闭,最简单的方式是定义闭包,闭包简而言之就是构造一个匿名函数并且立刻执行它。我们会得到执行结果,却不会将局部变量暴露给外层或全局。

优化后的代码

1
2
3
4
5
(function() {
var a = 1;
var b = 2;
alert(a+b);
})();

简单构造的闭包语法,可以很方便地将非闭包语法加以封闭,在调试阶段,确保变量不冲突的前提下,可将首尾行注释,方便在Chrome等开发工具中调试

1
2
3
4
5
//(function() {
var a = 1;
var b = 2;
alert(a+b);
//})();

封闭性是一个良好代码设计的开始,闭包内的变量默认是私有
(这里不考虑不用var声明的全局变量,参照JS代码规范)。
我们可以选择性地在闭包的结尾将公开成员公开出去。

1
2
3
4
5
6
7
(function() {
var pageOpenTime = new Date();
function getPageOpenTime() {
return pageOpenTime;
}
window.getPageOpenTime = getPageOpenTime;
})();

但也不是教条主义。比如我们要定义一组API,其实可以这么实现
但按照前面的规范,getNow()与getPageOpenTime()是互相独立的,而且getNow()中理应无法调用pageOpenTime。代码该写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function() {
var pageOpenTime = new Date();
function getPageOpenTime() {
return pageOpenTime;
}
window.getPageOpenTime = getPageOpenTime;
})();

(function(){
function getNow() {
return new Date();
}
window.getNow = getNow;
})();

那么恭喜你,你已经犯了教条主义。也就是过度设计。我们实现的API内部是几乎不可能被外界干预的。
没必要设过于复杂的限制约束,不论复杂度还是维护都麻烦。
这又是与封闭性互为反面的,如何权衡呢?
我们只需要保证对外公开的接口严格准确,内部是可以更灵活一些的。即便丧失了局部封闭性,但对外接口还是封闭的。而且也更加灵活。

那么我们最终实现的代码如下即可。

1
2
3
4
5
6
7
8
9
10
11
(function() {
var pageOpenTime = new Date();
function getPageOpenTime() {
return pageOpenTime;
}
function getNow() {
return new Date();
}
window.getPageOpenTime = getPageOpenTime;
window.getNow = getNow;
})();

再深入思考一下,我们把一坨函数直接暴露给window真的好么?倘若我们有100个函数,不敢想象。。
我们需要做一个包装对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function() {
var pageOpenTime = new Date();
function getPageOpenTime() {
return pageOpenTime;
}
function getNow() {
return new Date();
}
var TimeTools = {
getPageOpenTime: getPageOpenTime,
getNow: getNow
};
window.TimeTools = TimeTools
})();

这样就保证全局只有唯一一个暴露对象了。

那我们继续深入,万一有另外一个人也蛋疼的起了个叫TimeTools的全局变量,咋办???继续冲突中
我想说,非要把人往死路逼么?好吧,那我也拿出看家本领。参见jQuery.noConflict()方法
大意就是在赋值全局变量时,先保存之前版本的全局变量,如果冲突,可以调用noConflict()方法回滚全局变量,并且返回私有的工具对象

//我非要叫TimeTools,你咬我啊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var TimeTools = {
abc: 123
};
(function() {
var pageOpenTime = new Date();
function getPageOpenTime() {
return pageOpenTime;
}

function getNow() {
return new Date();
}

var TimeTools = {
getPageOpenTime: getPageOpenTime,
getNow: getNow
};

var oldTimeTools = window.TimeTools;
function noConflict() {
window.TimeTools = oldTimeTools;
return TimeTools;
}

TimeTools.noConflict = noConflict;

window.TimeTools = TimeTools
})();
//测试代码
(function() {
var TimeTools = window.TimeTools.noConflict();

console.log(TimeTools.getNow()); //我的API
console.log(window.TimeTools.abc); //原始页面上的全局变量
})();

通过如上层层深入的设计,我们可以保持js代码的封闭性,在开发底层库或者中间件的过程中,可以大大降低冲突的风险,更便于调试。