Javascript与模式

jopen 10年前

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'));

其在内存中的表现如下图

Javascript与模式

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;

这里使用了原型链,请看下面的内存结构图:

Javascript与模式

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。此模式的类图如下

Javascript与模式

以上是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