javascript物件導向開發(一),javascript物件導向
一、js物件導向簡介
js以它的靈活多變、易學易用而成為使用最廣的web前端指令碼語言,之前我在項目中只是用js進行資料校正、事件綁定或者dom等操作,然而隨著項目複雜度越來越高,不得不接觸到js物件導向的開發。雖然js在許多地方都有借鑒java的痕迹,但是就物件導向程式設計這方面,與java還是有著較大的差別。它並沒有諸如class、interface這樣的關鍵字,相反它非常單一,只用到了function這個關鍵詞,並且 它把類換了一個名字 ,叫做原型對象。我們在java中定義一個類,往往使用這樣的格式:
<pre name="code" class="java">class A{ private String name; ......}
在js中,我們採用如下的方法定義一個類:
function Cat(){//定義一個貓原型對象(類) } 如果我們進行Cat();調用,則將Cat作為一個普通的函數進行使用。如果var cat = new Cat();則將Cat作為一個類來使用。由於普通函數和類都是用同一個關鍵詞function進行修飾,代碼的可讀性會有一定的降低。因此我們一般約定如果是函數則首字母小寫(如cat),如果是類則首字母大寫(如Cat)。
在java中,我們一般習慣先定義類,然後建立類的執行個體進行使用。遵循先定義模具,再用模具生產產品這樣思想。但是js的卻不同,它非常鬆散,甚至可以動態為執行個體添加屬性和方法,如:
<pre name="code" class="javascript">//動態添加屬性var cat = new Cat();cat.name = "小白";alert(cat.name); //小白
//動態添加方法cat.sayHello = function(){ alert(this.name);}cat.sayHello(); //小白
這種方法雖然非常靈活,但是在某程度上違背了物件導向設計的原則,因此不建議這樣使用,最好還是模仿java中的做法,先定義類,再執行個體化對象。
在java中,我們擷取一個對象的屬性,往往採用 對象名.屬性名稱 進行擷取。在js中,除了可以這麼做,還可以使用對象名["屬性名稱"](注意[]中傳遞的是字串),這樣的好處可以進行動態訪問,我們可以利用這一特性進行一些技巧編程,後文會介紹到利用這一特性類比java中map的例子。當然,如果該對象並不存在某一屬性,如我們進行alert(cat.age);這樣的調用,會顯示undefined。
如果我們將cat這個執行個體賦值給另一個引用變數,如:var cat2 = cat,那麼cat和cat2都指向了同一個執行個體對象。如果我們把cat.name = "小白2",那麼cat2.name也將變成“小白2”,相信這個應該非常好理解。但是如果如果我們把cat2 = null那麼cat究竟會怎麼指向呢?它應該還會指向原來的堆,並不會也指向null,這點在編程中應該引起注意,我在以往項目中就吃過這個虧。
二、js建立對象的方式1、工廠方式
和java類似,js也有一個叫做Object的超類。我們可以不定義類,而是直接採用Object得到一個執行個體,然後採用上文所說的動態添加屬性和方法的方式進行開發。雖然這種方式在實際項目中用的並不多,但是作為最簡單的一種建立對象的方式,我認為還是有必要瞭解一下其實現方法的。我們來看如下的代碼:
var a = new Object(); //或者var a = {}a.name = "123";alert(a.name); //123 a.getName = function(){return this.name;}alert(a.getName()); //123
var a = new Object();這是一種標準的工廠方式建立對象的方式,當然我們也可以簡寫為:var a = {},兩種寫法等價,但是明顯第二種方法更加簡單,在許多底層代碼中,更多的會採用第二種的寫法。介紹這種模式的另一個目的我是想引出Object的一個方法:defineProperty,它也可以為執行個體對象添加屬性或方法,具體使用方法如下:
Object.defineProperty(a,"age",{writable:false, //false:唯讀 true:可修改configurable:true, //true:可以使用delete a.age刪除該屬性enumerable:true, //可以被枚舉value:13 //初始值});alert(a.age); //13 defineProperty可以接收3個參數,第一個為執行個體化對象,第二個為需要添加的屬性或方法名,第三個參數可以為屬性和方法設定一些屬性。上面的代碼的含義為:為a這個執行個體化對象添加一個age的屬性,並且它不可被修改、可以被刪除、可以被枚舉,它的值為13。我們在這裡暫時先介紹一下這種用法,下文在講到prototype的時候,我們會利用這種方法最佳化代碼。
2、建構函式模式
function B(name,age){this.name = name; //publicvar age = age; //privatethis.getName = function(){ //publicalert(this.name);}var getAge = function(){ //privatealert(this.age);}}var b = new B("tim",9);b.getName(); //tim b.getAge(); //報錯 利用建構函式定義類應該是日常應用程式開發中使用最多的一種方法。這種方法多少有一點點java的蹤影,這裡需要注意的是,js中屬性或者方法只有公有(public)和私人(private)兩種方式,並沒有protected。如果寫上var則說明是私人的,寫上 this. 則說明是公用的。因此getName能被成功調用而getAge無法被調用。
說明:js中的this一直是比較困惑人的關鍵詞,我對它的歸納是:誰調用了它,this就指向了誰。
這裡需要注意的是,使用建構函式定義類,每建立一個對象,都會拷貝一份代碼。也就是說如果我們建立了var b1 = new B("tim",9);和var b2 = new B("tom",10);這兩個執行個體化對象,它們所持有的代碼是不共用的,是互相獨立的,那麼必然會造成記憶體空間的浪費。這也是建構函式定義類最大的缺點。為了彌補這個缺陷,提出了prototype這個概念,通過它去定義類的屬性或方法,所有的對象將共用同一份代碼,大大減少了記憶體空間的開銷。
3、原型法
上文有提到prototype這個概念,它可以讓所有的執行個體化對象共用同一份代碼。我們先來看一下它的使用方法。
function C(){}C.prototype.name = "tom"; //類級屬性C.prototype.getName = function(){ //類級方法<span style="white-space:pre"></span>return this.name;} 原型法建立對象和調用對象屬性方法與建構函式模式是一樣的。唯一的不同是使用prototype屬性和方法是作用在類身上而不是對象身上,這與java的static的功能有些類似。我們需要注意的是prototype不能去訪問類的私人方法和屬性,比如我們為C這個類定義了一個私人變數age:var age = 10;然後再prototype上添加一個getAge的方法:C.prototype.getAge = function(){alert(age);} 這種方式是不允許的,如果調用它,會顯示undefined。
prototype還有一個很強的作用,就是可以擴充原來的類。我們知道,Array單體類為我們提供了一個forEach的方法用於遍曆數組元素,但是它無法處理多維陣列。我們可以利用prototype為Array添加一個處理多維陣列的方法each,那麼我們可以這麼做:
//為Array添加可以迴圈多維陣列的方法Array.prototype.each = function(fn){if(this && this.length > 0 && fn.constructor == Function){ //數組存在 且 長度大於0 且 傳遞的參數是一個函數(回呼函數)var index = 0;for(var i=0;i<this.length;i++){var element = this[i];if(element.constructor == Array){//仍然是一個數組,再一次調用each,形成遞迴element.each(fn);}else{fn.call(element,element,i);}}}}//使用arr = [1,2,[3,4,[5,6]]];arr.each(function(element,i){alert(element);}); 這裡我們需要區分建構函式定義的屬性與方法是作用在對象身上的,而prototype是作用在類身上的。其實prototype是一個指標,它指向了類原型對象。在這個原型對象中,我們可以添加屬性方法,此外它還持有了一個constructor屬性,它實質就是類的建構函式。下面我們通過虛擬碼的方式梳理一下建構函式、原型對象和執行個體對象之間的關係:
1、建構函式.prototype = 原型對象
2、原型對象.constructor = 建構函式模板
3、原型對象.isPrototypeOf(執行個體對象) = true
我們再來看一下下面這個執行個體,順便也介紹一些js中與物件導向有關的常用的API。
function C(){}C.prototype.name = "tom"; //所有執行個體共用var c = new C();alert("name" in c); //true 如果能訪問name,則為true 要和後面的hasOwnProperty區分開來alert(c.name); //tom 但是這個屬性不是屬於執行個體的而是屬於原型的alert(c.hasOwnProperty("name")); //false 因為是原型的c.name = "my"; //覆蓋原型屬性,name變成了自己的屬性alert(c.name); //myalert(c.hasOwnProperty("name")); //truedelete c.name ; //自己的name屬性沒了,但是原型的還在alert(c.name); //tomc.name = 111;alert(Object.keys(c)); //列印c自己的所有的屬性(不包括原型的),其傳回值為一個數組對象 說明:如果建構函式和prototype擁有同名的屬性,在調用時會優先查詢建構函式的。
4、重寫原型模式之前有講到過prototype是一個指標,指向了這個類的原型對象,既然如此,那麼我們是否可以改寫指標指向的東西呢?答案是可以的。我們來看一下下面這種定義類的方法:
function Person(){}//重寫Person的原型Person.prototype = {name:"py",age:11,sayHello : function(){alert(this.name);}};var person = new Person();person.sayHello();alert(person.constructor); //funciton Object(){}
我們可以看到,我們可以通過一種類似json的key-value的寫法定義類級的屬性和方法,有些人將其稱為簡單原型。相信大家閱讀這段代碼應該是比較輕鬆的,唯一的疑惑應該是最後行代碼。因為之前有說過prototype有一個constructor屬性,其值為類的建構函式本身,那麼理論上應該alert出function Person(){},怎麼會是Object呢?其原因是我們改寫了prototype指標,它的原型變了,變成了Object。如果想要變回Person可能大家第一想法就是在Person.prototype = {}中寫上:constructor:Person;。這種方法固然可行,但是在js中建構函式constructor是不能被枚舉的,我們這樣改寫將會破壞這個規矩。這時我們可以用上文提到過的defineProperty進行改寫。具體代碼如下:
function Person(){}//重寫Person的原型Person.prototype = {//constructor:Person,name:"panye",age:11,sayHello : function(){alert(this.name);}};Object.defineProperty(person,"constructor",{enumerable:false, //不可被枚舉value:Person}); 這樣再一次執行alert(person.constructor);將會彈出funciton Person(){}。
三、物件導向應用1、開發常用的物件導向設計方式上文提到建構函式模式建立對象會浪費一定的記憶體空間,因為它的代碼不共用。雖然prototype方式可以共用代碼,但是正所謂“成也蕭何,敗也蕭何”,這個特性也成為了它的弊端。比如我建立了兩個Person類的執行個體,我將其中的一個執行個體的name屬性改成“小白2”,那麼另一個執行個體的name屬性也會發生相應的變化,這是我們不願看到的。所以我們在正式開發中,往往結合兩者的做法,取長補短。我們知道,同一個類的不同執行個體,其本質的不同並不是在於方法的不同,而是在於屬性的不同。比如人與人之間,大家都會說話,都會走路,那麼互相之間的差異就在於我們的名字不同、體重不同。所以,我們在進行開發時,可以用建構函式的方式去定義屬性,用原型法定義方法。
function Person(name,age){//屬性不共用this.name = name;this.age = age;}//方法共用:Person.prototype = {constructor:Person, //可以用defineProperty進行最佳化改進//共用的方法sayHello:function(){alert(this.name + this.age);}}//建立執行個體var p1 = new Person("tim1",12);var p2 = new Person("tim2",14);p1.sayHello(); //tim112p2.sayHello(); //tim214
2、利用物件導向特性類比實現java的Mapjs中並沒有給我們提供集合類型,我們可以用數組等方式去類比Map,但是考慮到Map的特性:key-value的鍵值對形式並且key不能重複。我們完全可以利用js中類的特性去類比實現。
function Map(){var obj = new Object(); // 存放索引值對,把key作為obj的屬性,value作為屬性的值//添加方法this.put = function(key,value){obj[key] = value;}//擷取長度this.size = function(){var count = 0;for(var a in obj){count += 1;}return count;}//根據key擷取this.get = function(key){return obj[key];}//根據key刪除this.remove = function(key){delete obj[key];}//迴圈遍曆this.forEach = function(fn){ for(var a in obj){fn(a,obj[a]);}}}var map = new Map();map.put("01","aaa");map.put("02","vvv");map.put("03","bbb");alert(map.size()); //3alert(map.get("03")); //bbbmap.remove("03");alert(map.size()); //2alert(map.get("03")); //undfined map.forEach(function(key,value){alert(key+" " + value);}); 說明:以上的代碼只是想說明編程思想,在正式業務處理中有一些特殊情況為考慮在內,讀者若有興趣可完善代碼。
四、結束本次的部落格我介紹了js物件導向的基礎以及建立對象的幾種方式。在下一次部落格中我將會和大家分享js中如何?繼承與介面編程,並分享基於js的常見設計模式實現。
由於本人是web前端菜鳥,在部落格中若出現了錯誤的描述,請各位大牛指正!
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。