PHP 具備一些動態語言的特徵, 但不徹底. 雖然 PHP 的標誌是一頭大象, 可這頭象的鼻子未免太短, 以致經常夠不著東西, 反而象豬了. 本文旨在探討一種使 PHP 更動態化的方法, 主要是類比 Javascript 的 prototype 繼承. 既然是類比, 就不是真的能使 PHP 動態起來, 只是插上一根蔥, 讓它裝得更"象"一點.
一. 基本操作
通過 Javascript 的 prototype 動態地為對象添加屬性, 我們可以這樣:
Object.prototype.greeting = 'Hello'var o = new Objectalert(o.greeting)
Js 的內建對象 Object 可看作一個"類", 任何 Js "類"都有 prototype 內建對象, 用 PHP 來類比它可以是:
error_reporting(E_ALL);class Object{ public static $prototype; protected function __get($var) { if ( isset(self::$prototype->$var) ) { return self::$prototype->$var; }}}
然後我們可以:
Object::$prototype->greeting = 'Hello';$o = new Object;echo $o->greeting; // 輸出 Hello
這裡利用了 PHP 的自動轉型特性. 在 PHP 中, 我們要聲明一個數組, 並不需要先 $var = array() 然後才做 $var[] = some_value, 直接地使用後者就可以得到一個數組; 同樣地直接 $object->var 的時候, $object 就被自動定義為 stdClass 對象. 這就解決了在定義類內靜態屬性時不能聲明 public static $prototype = new stdClass 的問題.
在 Js 中給"類"動態添加方法:
Object.prototype.say = function(word) { alert(word) }o.say('Hi')
在 PHP 中類比:
error_reporting(E_ALL);class Object{ public static $prototype; protected function __get($var) { if ( isset(self::$prototype->$var) ) { return self::$prototype->$var; }} protected function __call($call, $params) { if ( isset(self::$prototype->$call) && is_callable(self::$prototype->$call) ) { return call_user_func_array(self::$prototype->$call, $params); } else { throw new Exception('Call to undefined method: ' . __CLASS__ . "::$call()"); }}}
這樣, 就可以
Object::$prototype->say = create_function('$word', 'echo $word;');$o->say('Hi');
但是 PHP 的 create_function 返回的結果並不等同於 Js 中的 Function 對象, Js 的 Function 對象是一種閉包(closure), 它可以直接調用宿主的屬性, 如
Object.prototype.rock = function() { alert(this.oops) }o.oops = 'Oops'o.rock()
但是在 PHP 中我們不可以寫
Object::$prototype->rock = create_function('', 'echo $this->oops;');$o->oops = 'Oops';$o->rock();
會報告 Fatal error: Using $this when not in object context, 因為 create_function 返回的是匿名的普通函數, 它沒有宿主. 為解決這個問題, 我們需要在參數中傳入對象本身, 而且不能使用 $this 變數名做參數, 我們暫時用一個 $caller 的變數名:
Object::$prototype->rock = create_function('$caller', 'echo $caller->oops;');$o->oops = 'Oops';$o->rock($o);
現在可以了, 可是看上去怪怪的, 一點都不像動態語言. 嗯~, 這根蔥還是有點短, 還是不"象".
問題來了:
1. 在調用動態方法時需要傳遞對象本身, 這算哪門子的物件導向?
2. 我們要在代碼中使用 $this, 這才象是在物件導向.
解決方案:
1. 重新寫一個函數代替 create_function, 在參數部分擠一個參數 $that 進去作為第一個參數, 在 __call 中向匿名函數傳遞參數時加入對象本身 $this 作為第一參數.
2. 允許在代碼中使用 $this, 我們在代替函數中把 $this 換成 $that.
我們給它添加一個 create_method 函數來代替 create_function
function create_method($args, $code) { if ( preg_match('/\$that\b/', $args) ) { throw new Exception('Using reserved word \'$that\' as argument'); } $args = preg_match('/^\s*$/s', $args) ? '$that' : '$that, '. $args; $code = preg_replace('/\$this\b/', '$that', $code); return create_function($args, $code); }
$that 作為參數中的"保留字", 當出現在參數部分中將拋出異常.(在 PHP5 的早期暗夜版本中, $that 也曾經是保留字)
相應地, Object 中的 __call 也要作出改動
class Object{ public static $prototype; protected function __get($var) { if ( isset(self::$prototype->$var) ) { return self::$prototype->$var; }} protected function __call($call, $params) { if ( isset(self::$prototype->$call) && is_callable(self::$prototype->$call) ) { array_unshift($params, $this); // 這裡! return call_user_func_array(self::$prototype->$call, $params); } else { throw new Exception('Call to undefined method: ' . __CLASS__ . "::$call()"); }}}
現在我們就可以
Object::$prototype->rock = create_method('', 'echo $this->oops;');$o->oops = 'Oops';$o->rock();
二. 繼承
物件導向的一大特徵是繼承, 繼承最大限度地保留代碼重用能力. 但如果直接用上例的 Object 類去建立繼承類則會出錯, 因為
1. 子類繼承的靜態屬性 $prototype 永遠屬於父類(不管 $prototype 是標量還是列表, 對象更不消說)
2. 如果子類所繼承的方法中有 self 關鍵字, self 會指向父類而非子類
class Object{ public static $prototype; protected function __get($var) { ... } protected function __call($call, $params) { ... }}class Test extends Object{}Test::$prototype->greeting = 'Hello';print_r(Object::$prototype);/* outputsstdClass Object( [greeting] => Hello)*/Test::$prototype->say = create_method('$word', 'echo $word;');$o = new Object;$o->say('Hi');/* outputsHi*/