This page looks plain and unstyled because you're using a non-standard compliant browser. To see it in its best form, please visit upgrade to a browser that supports web standards. It's free and painless.

Fillano's Learning Notes 會員登入 會員註冊

之前在用javascript試作memento pattern時碰到一個難題,就是不知道怎麼做出friend宣告的效果,無法向Caretaker封裝Memento中存放狀態的細節。這幾天試了一下,其實適當地使用closure,就可以做出類似的效果。

測試的程式:

(function(){
	var a;
	var b;
	var count=0;
	function c() {
		if(state===undefined) var state=new Array();
		this.index = ++count;

		a = function(idx) {
			return state[idx];
		}
		b = function(idx,_state) {
			state[idx] = _state;
		}
	}
	d = function() {
		var _c = new c();
		this.e = function() {
			return a(_c.index);
		}
		this.f = function(_state) {
			b(_c.index,_state);
		}
	}
})();
var g = new d();
var h = new d();
g.f(23);
h.f(35);
alert(g.e());
alert(h.e());
h.f(77);
alert(g.e());
alert(h.e());

這裡用了一個匿名函數來做出closure,變數a與b由於宣告在匿名函數裡面,所以可以被c與d這兩個function使用。然後在c()裡面才定義a與b是兩個函數,由於位於c()裡面,所以透過a()與b()就可以存取定義在c()裡面的state變數。而a與b一開始是宣告在匿名函數裡面,所以d()也可以使用,透過迂迴的方式,就讓d()可以透過a()與b()存取定義在c()裡面的state變數。另外,由於c()是定義在匿名函數裡面,在匿名函數之外是無法存取的,而d()是在匿名函數內定義的global變數,所以可以使用。這樣設計好,就可以使用d()來把狀態存在c()的state變數中。

但是d()可以有很多instance,每一個instance如果都把狀態存在state變數裡面,那後面的存取動作就會蓋過前面的,所以在匿名函數裡面會維護一個計數器count,同時在c()開頭在沒定義state變數時才宣告state變數,然後讓每一個c()的instance會有一個獨立的index,在d()裡面需要透過這個index來存取state變數。

大致測試了一下,看起來可以working沒問題。

如果要用這個方法來做出memento pattern,那需要把c()(也就是memento)的instance存在Caretaker裡面,由於c()只有一個成員index,並沒有狀態,所以也可以說對Caretaker適當地把狀態封裝起來了。

接著改寫一下之前實作的memento pattern:

(function(){
	var a;
	var b;
	var count = -1;
	var memento = function(state) {
		if(state===undefined) var state = new Array();
		this.index = ++count;
		state[this.idx] = state;
		a = function(idx) {
			return state[idx];
		}
		b = function(idx, _state) {
			state[idx] = _state;
		}
	}
	var _instance = null;
	getOriginator = function() {
		if(_instance==null) {
			_instance = new originator();
		}
		return _instance;
	}
	var originator = function() {
		var _state = 0;
		var _memento=null;
		this.createMemento = function() {
			_memento = new memento();
			b(_memento.index, _state);
			return _memento;
		}
		this.setMemento = function(obj) {
			if(obj instanceof memento) {
				_memento = obj;
				_state = a(_memento.index);
			}
		}
		this.getState = function() {
			return _state;
		}
		this.setState = function(state) {
			_state = state;
		}
	}
})();
function caretaker() {
	var _memento = null;
	this.execute = function(state) {
		var _originator = getOriginator();
		_memento = _originator.createMemento();
		_originator.setState(state);
		alert(_originator.getState());
	}
	this.unexecute = function() {
		var _originator = getOriginator();
		_originator.setMemento(_memento);
		alert(_originator.getState());
	}
}
var a = new caretaker();
a.execute(3);
a.execute(5);
a.unexecute();

跑完a.unexecute(),狀態有從5退回到3,看起來應該沒問題。

GOF的Memento Pattern裡面用了三個類別來協作,分別是Caretaker、Originator以及Memento。Originator依賴Memento來儲存狀態,並將Memento存在Caretaker,Caretaker操作Originator時,可以跟Originator取得Menento來存放,等到需要回復操作時,把Memento傳給Originator來回復他的狀態。

Caretaker對於Memento有存取的限制,在GoF的例子裡面,是把Originator指定為Memento的Friend類別,讓Originator可以存取Memento的getState,setState等私有方法,同時Memento的建構子也是私有的,所以只有透過Originator才能創建。

在Javascript中沒有Friend....倒是可以透過closure來讓Memento只有Originator才能創建,但是Memento的setState以及getState方法,就沒有辦法建立存取的權限,所以Caretaker收到memento時,其實可以直接使用他的....get/setState方法,這樣做就沒意義了。

GoF的例子裡面,Originator是一個Singleton,這個倒是可以用匿名函數的closure做出來就是了。

以下是測試的程式:

//試作memento模式,另外也用closure做出一個singleton
(function(){
	var memento = function(state) {
		var _state = state;
		this.getState = function(obj) {
			return _state;
		}
		this.setState = function(obj, state) {
			_state = state;
		}
	}
	var _instance = null;
	getOriginator = function() {
		if(_instance==null) {
			_instance = new originator();
		}
		return _instance;
	}
	var originator = function() {
		var _state = 0;
		var _memento = null;
		this.createMemento = function() {
			_memento = new memento(_state);
			return _memento;
		}
		this.setMemento = function(obj) {
			if(obj instanceof memento) {
				_state = obj.getState();
			}
		}
		this.getState = function() {
			return _state;
		}
		this.setState = function(state) {
			_state = state;
		}
	}
})();
function caretaker() {
	var _memento = null;
	this.execute = function(state) {
		var _originator = getOriginator();
		_memento = _originator.createMemento();
		_originator.setState(state);
		alert(_originator.getState());
	}
	this.unexecute = function() {
		var _originator = getOriginator();
		_originator.setMemento(_memento);
		alert(_originator.getState());
	}
}
var a = new caretaker();
a.execute(3);
a.execute(5);
a.unexecute();

所以結論是....失敗了。


2008-5-1 補充

後來用closure模擬出friend的效果,完整的memento pattern試作請見用closure模擬friend宣告的效果

關於物件導向javascript的繼承,應該是被討論最多的主題了。有興趣的話,Core JavaScript Guide文件裡面有非常詳細的說明,基本上按照它的說明就可以實作出來。(請參考裡面的Details of the Object Model章節)

底下用一個最簡單的例子做出繼承的效果:

function ancestor(_name) {
	this.name = _name||"";
}
function child(_name, _generation) {
	this.base = ancestor;
	this.base(_name);
	this.generation = _generation||0;
}
child.prototype = new ancestor;

這個方式,就是javascript經典的prototype base繼承。透過指定prototype屬性,便可以指定要繼承的目標。在child中,利用:

	this.base = ancestor;
	this.base(_name);
這兩行便可以呼叫父類別的constructor。原理是,this.base會assign成function ancestor,然後執行。執行時,傳給這個function的this會是child的instance的execution context,所以就把ancestor裡面做的事情在child裡面也重複了一遍。除了用這個方法以外,也可以透過呼叫this.call來達成。(不能使用this.constructor,因為它永遠指向繼承關係最上層的constructor,這樣在繼承超過兩層就會出問題)

接下來看一下prototype繼承的可能方式。其實可以繼承一個....匿名函數,例如:

function test(_task,_name) {
	this.constructor(_name);
	this.task = _task;
}
test.prototype = new (function(_name){
	this.name = _name||"";
});
var a = new test("coding","fillano");
alert(a.name);
alert(a.task);

執行上面的例子,會依序跳出"fillano"、"coding"訊息對話框。(抱歉,這裡用了this.constructor,超過兩層繼承會出問題。但是不這樣的話,沒辦法傳參給anonymous function的constructor...。所以繼承一個anonymous function並不是個好主意,只是做的到而已。)

也可以繼承一個Native Object例如....window物件:

function Workerbee() {
	this.task = [];
	this.tid = null;
	this.test = function() {
		this.tid = this.setTimeout(this.test1,100);
	}
	this.test1 = function() {
		this.clearTimeout(this.tid);
		this.alert("you got me");
	}
}
Workerbee.prototype = window;
var a = new Workerbee();
a.alert("test");
a.test();

執行上面的例子,會依序跳出"test"、"you gotme"訊息對話框。(其他javascript的native object則要用new喔。其實很多native object無法這樣繼承的,它會做內部檢查,不讓你用它的constructor傳參數給他,也會檢查執行方法的物件型別,型別不是自己就出現錯誤。所以一般是不會繼承native object的,而是用object.prototype.x=y的方式加料上去。)

Core JavaScript Guide還提到不少要實作出不同效果的一些方法細節,像是用new產生instance時,在constructor裡面遞增一個計數;如何透過object.prototype.propertyName=value的方式,一次修改所有繼承體系中的propertyName的值等等(方法:propertyName屬性必須在constructor外面利用object.prototype.propertyName的方式定義)。

其實還有更動態的繼承方法,看看下面的例子:

function Parent(_name) {
	this.name = _name||"";
	this.show = function() {
		alert(this.name);
	}
}
function Child(_gender) {
	this.gender = _gender;
	this.mate = function() {
		alert(this.gender);
	}
	this.inherits = function(obj) {
		for(var i in (new obj)) {
			this[i] = (new obj)[i];
		}
	}
}
//for(var i in (new Parent)) {
//	Child.prototype[i] = (new Parent)[i];
//}
var a = new Child("male");
a.inherits(Parent);
a.name = "fillano";
a.show();

如果不需要使用constructor,可以用這方法把另一個物件中的所有屬性與方法拷貝到目標物件中。在javascript裡面,assign這個動作是無遠弗屆的!!!(註解的程式與inhertis方法是同義的。另外,這個方法對於window無效。其實要拷貝constructor也是可以的,功能比較完整的例子,可以參考Douglas Crockford的文章:Classical Inheritance in Javascript,裡面有詳細地討論javascript的繼承。除了Core JavaScript Guide之外,這篇應該是物件導向Javascript的必讀文章了。Crockford還有許多討論Javascript特性的文章,都很值得一讀。個人很多觀念都是這裡學來的。我這裡用的方法只是簡單地看出效果,要實用的話,最好參考Crockford的方法。另外,我以前在google feed api產生的javascript也看過類似的作法:))

在Core JavaScript Guide中有提到,prototype base的繼承方法不支援多重繼承,因為改了prototype以後,整個prototype chain就改了,永遠就只有一條single chain的prototype chain,所以永遠也不能多重繼承。但是因為javascript動態的特性,其實用上面這個方法,是可達到多重繼承的目的,只是無法利用到constructor。

其實仔細研究這些繼承的過程,可以發現,javascript只是依照我們要它做的事情一一做好,而透過這些動作,就可以做出我們要的物件導向效果。所以要達到我們想要的目的,就必須一一按照必要的方式把它做出來。prototype繼承、封裝等都是適當地安排好程式做出來的效果。適當使用var、this、function等就可以做出資料封裝的效果;同樣適當地使用prototype、constructor等就能做出繼承的效果。但是這些與原生的物件導向語言例如.....java其實有很大的不同,所以必須很清楚這樣做是為什麼,做出這些效果跟程式運作的來龍去脈有甚麼關係等等,否則有可能不小心就破壞了這些效果,或是達不到目的。

簡單的結論:

  1. javascript有prototype base繼承與動態copy(assign)繼承兩種繼承方法
  2. 必須熟悉javascript的規則,然後再應用這些規則來做出物件導向的效果。