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>
其在內存中的表現如下圖
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>
這里使用了原型鏈,請看下面的內存結構圖:
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。此模式的類圖如下
以上是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