Javascript与模式
Java的模式靠着封装,继承,抽象和多态,实现了各种各样的设计模式。Javascript这种弱类型,解释性语言,靠着闭包和原型实现了自己的类的特性以及模式。
随着网络速度与电脑速度的增加,网站开始往富客户端方向发展。网站已经不在单纯的是内容展示,还有更眼花缭乱的展现方式,灵活巧妙的用户交互形式。这依靠大量的CSS与Javascript来实现。浏览器端的javascript,服务器端javascript也开始活跃,像现在比较活的node.js。以前的javascript代码量并不多,只是几个函数。现在的javascript代码量可以增多,一个web2.0的网站,javascript的代码量远远超过了html,更有甚者,有的网站js的代码量超过后台的java代码量。代码的增多,紧紧使用函数书写格式,是无法维护的。于是javascript开始模块化,开始注重各种模式的实现。
闭包(Closure)和原型(Prototype)
Javascript是通过闭包和原型玩出了各种花样。首先回忆一下闭包吧。
var a=0; //#1 var myFunc = (function(){ var a=1; //#2 return (function(){ var a=2; //#3 return (function(){return a}); }()); }()); var result = myFunc(); console.log(result);
上面的代码会打印2,如果把#3一行删掉,则打印1,如果再把#2一行删掉,则打印0.
通过这段代码充分展示了什么叫做闭包。我的理解为:
闭包就是一个对象与对象的上下文的集合。代码中的对象就是最后返回的函数(function(){return a}),而上下文则是函数定义时其环境中所有定义的参数,换一个专业的词来讲,就是其函数的scope chain.
原型prototype是为节省内存而产生。看下面的例子
function MyClass() { this.v1='v1'; } MyClass.prototype.v2='v2'; var a=new MyClass(); var b=new MyClass(); MyClass.prototype.v3='v3'; a.v3='a3'; console.log(a.v3); console.log(b.v3); console.log(a.hasOwnProperty('v3')); console.log(b.hasOwnProperty('v3'));
其在内存中的表现如下图
v1直接添加到this上,这使得每次new一个实例,都会为v1分配一个内存。v2生命在prototype中,只使用一个内存。而a.v3会将v3添加到a的实例里去。
请自行运行以上代码,通过打印结果,结合内存示意图理解prototype.
对象创建模式
在java中,一个类会存在构造函数,静态变量与方法,公有变量与方法,私有变量与方法。在javascript中,我们可以使用闭包来实现这些特性。
var MyClass = function(){ // Private var privateMethod=function(){ console.log('this is private method'); }; var privateVariable=1; // constructor var declareClass = function() { console.log('this is constructor.'); }; // public declareClass.prototype.publicVariable=2; declareClass.prototype.publicMethod=function(){privateMethod();}; declareClass.prototype.getPrivateVariable=function(){console.log(privateVariable);}; // static declareClass.staticVariable=3; return declareClass; }(); var instance = new MyClass(); instance.publicMethod(); instance.getPrivateVariable(); console.log(MyClass.staticVariable); console.log(typeof instance.privateMethod); console.log(typeof instance.privateVariable);
上面的代码使用即时函数的执行体来定义一个类,即时函数内部对类形成了一个闭包,闭包内所有的定义均是潜在的私有变量或方法。最后把定义好的类返回时,可以自行决定提供哪些共有类和方法。
继承
通常,javascript通过两种方式实现继承,一是类式继承,二是混入(mix in).
类式继承
类式继承就有很多讲究,有多重方法可以实现类式继承。但它们本质都离不开prototype已经javascript提供的两个函数apply和call. 这里我只讲了一种方式,使用原型实现继承。我们从中得到启发,去设计出更多的继承方法。
var Parent=function(){}; Parent.prototype.func=function(){}; var Child=function(){}; Child.prototype=new Parent(); Child.prototype.constructor=Child;
这里使用了原型链,请看下面的内存结构图:
Child的实例在调用this.method的时候,会从左向右搜索。左边的变量与方法覆盖右边的变量与方法。这种方法的缺点是Parent定义中所有定义的变量和方法都被继承了。比如this.a.
接下来,把以上的代码重构一下,使得类的声明更加的标准化,就像dojo中的dojo.declare一样。
declare = function declare(name, parent, body) { var f =function(){}; if (typeof parent === 'function') { f.prototype=new parent(); f.prototype.constructor=this[name]; } for(var key in body) { f.prototype[key]=body[key]; } this[name]=f; }; declare('Parent', null, { func:function(){console.log(this.name);}, name:'Parent' }); declare('Child', Parent, { name:'Child' }); var p = new Parent(); p.func(); var c = new Child(); c.func();
混入(mix-in)
类式继承最大的缺点是单继承,如果我想实现多继承,就需要混入模式了。混入非常的简单,就是将父类所有的函数,全部复制到子类中,并把父类prototype中的函数也复制到子类的prototype中,这就是混入。代码就不演示了,混入的缺点是如果多个父类含有相同的方法或者属性,你必须决定要保留哪一个。
单例模式与观察者模式
上面讨论了javascript如何实现对象创建与继承,接下来,我们使用以上知识,开始实现一些设计模式。设计模式有很多,GOF给出了各种模式的定义。这里挑选出两个和JS最相关的模式来讲。单例模式充分巧妙的运用了JS的闭包,有利于我们加深对JS设计的理解。而观察者模式则是JS中使用最多的模式。Browser端的JS编程,主要运用了大量的观察者模式。
单例模式
先看第一个例子
function Singleton(){ if (typeof Singleton.instance === 'object') { return Singleton.instance; } this.method=function(){}; // Do you job Singleton.instance=this; } var inst = new Singleton(); var inst2 = new Singleton(); console.log(inst===inst2);
上面的代码使用静态变量存放单例实例。这跟java中的单例模式思想一致。但JS没有私有静态变量,所以Singleton.instance可以被任意改写,这是不安全的。
针对上例的不安全,给出下面的例子
function Singleton(){ var instance = this; // do something //... Singleton=function(){ return instance; } instance.constructor=Singleton; return instance; } var inst = new Singleton(); var inst2 = new Singleton(); console.log(inst===inst2);
上例的巧妙之处在于Singleton为一次性函数,它在运行时,自我发生了改变。
除了以上两个例子,我们还可以使用闭包来实现单例模式,将单例实例当做一个私有变量:
var Singleton = function(){ var instance; return function(){ if(instance) return instance; instance=this; // do your things now //... } }(); var i1=new Singleton(); var i2=new Singleton(); console.log(i1===i2);
观察者模式
浏览器端的JS使用了大量的观察者模式。观察者模式包含Subject和Observer。此模式的类图如下
以上是java中典型实现。在原生的浏览器端DOM上的事件处理则遵循window.addEventListener(),老版本的IE使用attachEvent. dojo还提供了扩展的观察者模式dojo.subscribe和dojo.publish,以及dojo.connect.
下面是一段我实现的代码, 容错性不高,只是解释下观察者模式的实现。
var Subject = { topics:{}, subscribe:function(topic, fn, context){ if (!this.topics.hasOwnProperty(topic)){ this.topics[topic]=[]; } this.topics[topic].push({fuc:fn, ctx:context}); }, publish:function(topic){ var list = this.topics[topic]; var length = list.length; for(var i=0; i<length; i++) { var f=list[i].fuc var ctx=list[i].ctx; f.apply(ctx, []); } } } var listener = { msg:'hello world', sayHello:function(){console.log(this.msg);} } Subject.subscribe('hello',listener.sayHello, listener); Subject.publish('hello');
模块化编程
看到模块化编程,想到了common JS, AMD等一系列规范。AMD的确是模块化编程。模块化编程提供了沙盒式运行空间,使得JS的每段功能代码均运行在自己的命名空间内,这样不会出现命名冲突,有效管理各个模块之间的依赖,并实现动态加载。在文章开头的第一段代码中,已经看到了模块的雏形,即使用即时函数定义一个类。所有的命名都被约束在了闭包中,不会影响到闭包以外的变量与函数。接下来,我要改进第一段代码,使得模块之间的依赖性得到自动解决。一个模块实现了一个功能集合,模块可能会返回一个借口,供依赖者调用模块中的功能,也可能什么也不返回,只是单纯的运行模块中的程序。接下来,将有四段代码,第一段实现了模块的声明,第二段实现模块的执行,第三段定义一个具有打印功能的模块,第四段执行一个模块,这个模块依靠打印模块执行打印功能。注意,没有考虑容错性。
模块的声明函数
var define=function(name, dependencies, fn) { var args=[]; var length = dependencies.length; for(var i=0;i<length;i++){ var dep=define.modules[dependencies[i]]; args.push(dep); } define.modules[name]=fn.apply(null,args); } define.modules={};
模块执行函数
var execute=function(dependencies, fn) { var args=[]; var length = dependencies.length; for(var i=0;i<length;i++){ var dep=define.modules[dependencies[i]]; args.push(dep); } fn.apply(null,args); }
打印机模块声明
define('com.test.Printer', [], function(){ return { print:function(msg){console.log(msg);} } });
执行模块
execute(['com.test.Printer'], function(printer){ printer.print('hello world'); });
将以上代码合并在一起执行。执行模块会执行代码并产生输出。
上面的代码属于相对简单的,它只给出了一个模块化的示意。想象一下,execute和define均考虑到了直接依赖,如果依赖又存在依赖,那这就属于一个递归的加载过程。另外,如果把每个模块都单独的存于独立JS文件中,那dependencies的加载就更加的动态,可以根据模块是否被依赖,而动态的加载模块所在的JS文件。这些都是可以被实现的。dojo的AMD已经实现了此动态加载功能。感兴趣的话可以去读一下源码。
来自:http://my.oschina.net/xpbug/blog/186355