100行代码让您学会JavaScript原生的Proxy设计模式
面向对象设计里的设计模式之Proxy(代理)模式,相信很多朋友已经很熟悉了。
其实和Java一样,JavaScript从语言层面来讲,也提供了对代理这个设计模式的原生支持。我们用一个不到100行代码的例子来看看吧。
下面的代码创建了一个名叫Jerry的Employee对象,然后用函数hireEmployee雇用该Employee进行JavaScript开发:
function Employee(name){
this.name = name;
};
Employee.prototype.work = function(language){
console.log(this.name + " is developing with: " + language);
}
let jerry = new Employee("Jerry");
function hireEmployee(employee, language){
employee.work(language);
}
hireEmployee(jerry, "JavaScript");
打印输出:
Jerry is developing with: JavaScript
现在Jerry在他的业余时间里想学习一些其他的编程语言,但是不想影响自己的本职工作。用技术语言来讲,就是希望Employee原型方法work执行时,打印一行额外的信息,但是不允许修改Employee函数和Employee.prototype.work本身。这时Proxy这种代理模式就派上用场了。
我们为work方法创建一个代理逻辑:
var proxyLogic = {
get: function(target, name) {
if( name == "work"){
var oriFun = target[name].bind(target);
return function(language){
oriFun(language);
console.log("and also study other language in spare time");
}
}
}
}
;
重点看第二行的get方法。两个输入参数,target和name。Target代表当前执行方法的实例,即方法调用者。Name代表具体的方法名称。第4行我们把原始方法取出来,存放到变量oriFun里。第五行返回一个新的JavaScript函数,该函数体的实现逻辑为首先在第六行调用原始方法,然后在第七行执行额外的逻辑。
大家在回忆我之前介绍Java InvocationHandler实现动态代理的文章:
Java动态代理之InvocationHandler最简单的入门教程
是不是思路完全一样?都是在代理逻辑里调用原始方法,然后再执行额外的代码。
这个proxyLogic生成后,怎么把它同我们原始的需要被代理的代码关联起来呢?
只需要1行代码:
var jerryProxy = new Proxy(jerry, proxyLogic );
Proxy函数是JavaScript提供的原生代理构造器,需要两个输入参数:
第一个输入参数是我们的Employee实例,即需要被代码的对象实例,第二个输入参数是我们开发好的代理逻辑。返回的即是装配好的代理对象,该代理对象的work方法实现在第二个输入参数里。
现在我们再次调用hireEmployee,传入Proxy构造器返回的代理对象:
hireEmployee(jerryProxy, “JavaScript”);
打印输出,代理逻辑生效了:
和Java的Invocation一样优雅地实现了代理设计模式。
再来了解 JavaScript 的一个设计模式。
设计模式(Design Pattern)中的桥接模式,有的朋友平时工作可能很少用到。桥接模式的核心在于将抽象部分和它的实现部分分离,使它们都可以独立的变化。听起来很抽象,让我们看一个具体而简单的例子,通过这个例子一步步的完善来加深对桥接模式的理解。
很多论坛点登录按钮时,
周围背景都会暗下来,这样可以突出即将弹出的登录框,让用户把精力集中在用户名和密码的输入上去。
很多论坛对于这种背景变暗的UI实现,是创建了一个HTML原生的div元素,加上一些精心设计过背景颜色的CSS样式来完成的。
我们下面称这种div元素为遮罩层div元素,即mask div。
下面讨论创建mask div的最优解。
实现版本1
创建一个createMask函数,作为登录按钮的事件响应函数。每次点击按钮之后执行该函数。
var createMask = function(){
return document.body.appendChild( document. createElement('div') );
}
$(‘#logon_button').click(function(){
var mask = createMask();
mask.show();
})
版本1的缺点
每次点击按钮都会创建一个mask div。当然一般情况下登录按钮只会点击一次。但是在面试场景中,面试官可能会把这个问题的讨论引导到其他方向上。如何实现即使多次点击按钮,也只会创建一次mask div?于是就有了版本2。
实现版本2
事先创建好一个mask div,放到一个全局变量里保存。这种方式有点像单例模式(singleton)的饿汉式单例。
var mask = document.body.appendChild(document.createElement('div' ) );
$( '#logon_button').click(function(){
mask.show();
})
版本2的缺点
版本2采用了一个全局变量保存事先创建好的mask div。还记得那句话么?全局变量是万恶之源。
另外,假设用户永远不点登录按钮,只是以游客身份浏览网站,那么这个mask div就白白创建了。
实现版本3
var mask;
var createMask = function(){
if(mask)
return mask;
else{
mask = document,body.appendChild( document.createElement('div') );
return mask;
}
}
版本3的缺点
虽然使用了饱汉式单例模式,避免了mask div在没有点击登录按钮的情况下不必要的创建,但还是使用了全局变量来存放mask div。要记住我们现在是在用JavaScript,因此可以用它提供的强大的闭包特性(closure)来实现不需要全局变量的饱汉式单例模式。
实现版本4
var createMask = function() {
var mask;
return function() {
return mask || ( mask = document.body.appendChild(document.createElement('div')));
}
}();
借助JavaScript的闭包特性,我们在第二行创建的自由变量(Free variable)只在闭包内部可见,外部消费者感知不到这个变量,因此成为存储mask div的最佳选择。看起来这个版本已经很完美了?不,它仍然有可以优化的空间,即题目提到的桥接模式。
版本4的缺点
从单一职责原理(Single Responsibility)来衡量版本4,createMask函数里实际包含了两种不同类型的逻辑:
1. 创建mask div
2. 使该mask div “单例化”
我们下面使用桥接模式将这两种逻辑分开,来实现最终版本。
使用桥接模式的实现版本5
这个实现包含了三个JavaScript函数。首先看singleton函数。
函数singleton的输入参数是另一个JavaScript函数(我称其为原始函数),输出是一个包装后的函数,其内部使用闭包,将原始函数第一次执行的结果保存在闭包内,当包装后的函数第二次执行时,直接返回闭包内保存的第一次执行结果。我们可以把singleton函数当成一个构造器,传入任意一个具有返回值的JavaScript函数,负责生产出具有“单例化”特性的新函数。
var singleton = function(fn){
var result;
return function() {
return result || ( result = fn.apply(this,arguments));
}
}
var origin = function(){
return document.body.appendChild(document.createElement('div'));
};
var createMask = singleton(origin);
然后我们调用这个singleton函数,把我们原始的创建mask div的函数origin作为参数传进去,得到加工后的新函数createMask。
这个例子体现了桥接模式的作用。我们通过singleton这个单例化构造器函数,成功将业务逻辑(创建mask div)和单例化这个纯技术需求分离开,这样也满足了单一职责(single responsibility)的设计理念。
要获取更多Jerry的原创技术文章,请关注公众号"汪子熙"。
- 点赞
- 收藏
- 关注作者
评论(0)