iT邦幫忙

DAY 4
3

Javascript面面觀系列 第 4

Javascript面面觀:核心篇《物件導向》

  • 分享至 

  • xImage
  •  

物件導向是現代程式語言很被重視的能力,Javascript可以怎樣支援物件導向呢?
封裝(encapsulation)
封裝的目的是要隱藏實作的細節,只讓抽象的介面暴露出來。對一般使用者來說,只要知道如何操作這些介面就可以了。

但是在Javascript中,物件的property與method都是...公開的,而且可以輕易被覆寫。像下面的例子:

 if (!oldOpen) {
     oldOpen = window.open;
     window.open = function(u,t,o) {
         return null;
     };
 }

這樣就會覆寫過window物件的open()方法,接下來網頁中的javascript就無法另外開啟視窗了。這是過去防毒軟體跟瀏覽器常用的擋廣告popup視窗的方法。(現在應該不是這樣搞的)

有一個簡單的方法可以達到封裝的目的,就是透過變數及函數。

像這個簡單的例子:(iron009.html)

 var F = function() {
 	var name;
 	this.setName = function(v) {name=v;};
 	this.getName = function() {return name;};
 };
 var a = new F();
 a.setName('test');
 alert(a.getName());

因為變數範圍的關係,在F函數之外沒有辦法直接存取name這個變數,只能透過getName()跟setName()方法。

不過這樣有一個壞處,就是基於Javascript的Lexical Scope特性,如果setName()與getName()是在F之外用F.prototype.setName=function(){...}以及F.prototype.getName = function(){...}這樣的方式來定義時,他們就無法存取name變數。如果要可以做到,那得多花一些功夫用Function物件的call或apply函數來實現:

 var F = function() {
 	var name;
 	this.fn = {};
 	function iter2a(it) {
 		var i=0;
 		str = '';
 		for (; i<it.length; i++) {
 			if (!i == (it.length-1)) {
 				str += it[i] + ",";
 			} else {
 				str += it[i];
 			}
 		}
 		return str.split(',');
 	}
 	function enum(em) {
 		var str = '';
 		for (var i in em) {
 			str += '[' + i + "]\n";
 		}
 		return str;
 	}
 	this.extend = function(n,f) {
 		if (typeof this[n] == 'undefined') {
 			this[n] = function() {
 				return f.apply(this, iter2a(arguments));
 			}
 		}
 	}
 };
 var a = new F();
 a.extend('getName',function(){return name;});
 a.extend('setName',function(v){name=v;});
 a.setName('test');
 alert(a.getName());

(iter2a這個函數是要把一些list轉成真正的陣列,enum則是列舉物件的property,作為內部測試使用)

除了使用變數,其實直接用function而不是用this.functionname = function來定義的函數,也只有在F之中才能存取。但是這些在使用透過extend方法加入的方法中,因為使用了apply改變了他運作的變數範圍,所以就可以直接存取。

簡單地說,用上述的方法,就可以把本地變數跟函數,當作private property及private method來用,達到封裝的目標。而透過使用extend方法加入的操作,仍然可以使用到這些private的資源。

繼承(inheritance)
Javascript支援prototype base的繼承方式,這些都是透過函數物件來做,所以Javascript的函數物件也可以把當看作類似class的角色。

最基本的繼承語法像這樣:

 function Parent() {
     this.foo = function(){
         alert('Parent.foo');
     };
 }
 Parent.prototype.bar = function() {
     alert('Parent.bar');
 }
 function Child() {
     this.myfoo = function() {
         alert('Child.myfoo');
     };
 }
 Child.prototype = new Parent();
 var a = new Child();
 a.foo();
 a.bar();
 a.myfoo();

(從Netscape的Core Javascript Guide開始就這樣建議這樣達到繼承的目的)

但是其實可以用更「手工」的方法:

 function Parent() {
     this.foo = function(){
         alert('Parent.foo');
     };
 }
 Parent.prototype.bar = function() {
     alert('Parent.bar');
 }
 function Child() {
     Parent.call(this)
     this.myfoo = function() {
         alert('Child.myfoo');
     };
 }
 Child.prototype = Parent.prototype;
 var a = new Child();
 a.foo();
 a.bar();
 a.myfoo();

兩個方法比較一下,更能了解Javascript的prototype繼承機制。簡單地說new Parent()這個動作,會執行Parent這個函數的動作產生一個物件,然後把Parent的prototype屬性指派給它後回傳。所以Child.prototype裡面,就是這個新產生的物件,以及這個物件的prototype屬性。之後在執行new Child()時也是一樣的。

多型(polymorphism)
javascript並不支援函數/方法參數的多型,只能支援override的多型。也就是說,子物件可以繼承父物件的方法或屬性,然後需要時可以override。繼續上節的例子,把子物件的myfoo與mybar改成foo與bar的話,a.foo及a.bar呼叫的就是子物件中定義的方法了。wiki中把這個叫做override的多型:http://en.wikipedia.org/wiki/Polymorphism_in_object-oriented_programming,所以把接下來的分享放到這一節。

Javascript透過prototype chain的方式達成override多型,其實原理很簡單,碰到一個成員敘述時,會先在物件本身找是否有這個identifier的定義,然後再到prototype屬性中找,如果有繼承,那prototype屬性就是父物件,還找不到,就到父物件的prototype屬性裡面找...等等。(剛剛在繼承的部份應該看得很清楚了,每個子物件的prototype屬性就是他的父物件實體,父物件實體也有prototype屬性,就是他的父物件...如此會形成一個「子物件.prototype.prototype.prototype....」的結構,所以叫做prototype chain)。只要在子物件裡面找到identifier的定義,就不會繼續找下去,所以就產生了override的效果。

如果子物件還想執行父物件中被override的方法怎麼辦呢?可以修改一下prototype繼承的做法:

 var Parent = function() {
 	this.foo = function() {
 		alert('Parent.foo');
 	};
 };
 Parent.prototype = Parent;
 Parent.bar = function() {
 	alert('Parent.bar');
 };
 var Child = function() {
 	Parent.call(this);
 	this.foo = function() {
 		this.superClass.foo.call(this);
 		alert('Child.foo');
 	};
 };
 Child.prototype = new Parent();
 Child.prototype.superClass = new Parent();
 Child.prototype.bar = function() {
 	alert('Child.fbar');
 };
 var a = new Child();
 a.foo();
 a.bar();

這樣就可以透過superClass屬性存取Parent,而不會被prototype chain規則擋住。

更動態的繼承
Crockford的Javascript:優良部份有介紹一些更動態的繼承方法,簡單地說,就是用for...in來列舉父物件的屬性與方法,然後拷貝到子物件裡。另外,可以用hasOwnProperty來過濾掉從prototype chain來的東西。一般來說,除非需要很嚴謹龐大的繼承體系,我們其實也很少用到Javascript的繼承,在需要的時候,通常只要做像這樣的動態繼承其實就夠了。

Javascript只有prototype繼承的能力,但是很多人還是比較喜歡用class繼承,如果是這樣的話,可以試試Prototype這個框架:http://www.prototypejs.org/,透過他的Class相關API,可以讓你用比較像class繼承的方式來使用Javascript,不過當然底層還是使用prototype繼承的。


上一篇
Javascript面面觀:核心篇《高階函數》
下一篇
Javascript面面觀:核心篇《Timer與事件-單執行緒模型》
系列文
Javascript面面觀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言