Javascript與模式

jopen 11年前發布 | 20K 次閱讀 JavaScript開發 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);</pre>

上面的代碼會打印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'));</pre>

其在內存中的表現如下圖

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);</pre>

上面的代碼使用即時函數的執行體來定義一個類,即時函數內部對類形成了一個閉包,閉包內所有的定義均是潛在的私有變量或方法。最后把定義好的類返回時,可以自行決定提供哪些共有類和方法。

繼承

通常,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;</pre>

這里使用了原型鏈,請看下面的內存結構圖:

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();</pre>

混入(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);</pre>

上面的代碼使用靜態變量存放單例實例。這跟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);</pre>

上例的巧妙之處在于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);</pre>

觀察者模式

瀏覽器端的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');</pre>

模塊化編程

看到模塊化編程,想到了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={};</pre>

模塊執行函數

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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!