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与模式_第1张图片

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与模式_第2张图片

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与模式_第3张图片

以上是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已经实现了此动态加载功能。感兴趣的话可以去读一下源码。

你可能感兴趣的:(JavaScript,模式)