JavaScript學習總結【8】面向對象編程

1、什麼是面向對象編程

  要理解面向對象,得先搞清楚什麼是對象,首先需要明確一點這裏所說的對象,不是生活中的搞男女朋友對象,面向對象就是面向着對象,換在代碼中,就是一段代碼相中了另一段代碼,自此夜以繼日的含情脈脈的面向着這一段代碼,這就叫做面向對象,誰要這麼給人解釋,那笑話可就鬧大了,但是可以把男朋友或者女朋友視爲一個對象,之前我們也簡單的介紹過對象,即可以把一個人視爲一個對象,對象有他的屬性和方法,屬性如:性別、身高、體重、籍貫等,方法有走、跑、跳等。那麼我們就可以從兩方面理解對象:

  (1)、從對象本身理解,對象就是單個實物的抽象。

  一本書、一輛車、一臺電視可以被視爲對象,一張網頁、一個數據庫、一個服務器請求也可以被視爲一個對象,當實物被抽象成對象,那麼實物之間的關係就變成了對象之間的關係,從而就可以模擬現實情況,針對"對象"進行編程。

  (2)、從對象的性質理解,對象是一個容器,包含屬性和方法。

  所謂屬性,就是對象的狀態,所謂方法,就是對象的行爲(完成某種任務),比如,我們可以把動物抽象爲對象,屬性記錄具體是哪一種動物,方法表示動物的行爲,比如:捕獵、奔跑、攻擊、飛、爬、休息等。

  總體來講,對象是一個整體,對外提供一些操作,比如電視,我們並不瞭解其內部構成以及工作原理,但是我們都會使用,對於電視來說,只要用好按鈕,會操作,這個電路那個元件怎麼工作,跟我們沒什麼關係,只要電視能正常運行就好了,我們只要知道每個按鈕是幹嘛的,就可以使用這些功能,這就是面向對象。再比如獲取時間 Date,通過不同的屬性我們可以獲取到不同的時間,比如年份月份星期,我們並不知道他具體是怎麼實現的,但是都知道使用哪個屬性可以獲取到所需要的,這就是面向對象。

  那到底什麼是面向對象?簡單說就是在不瞭解內部原理的情況下,會使用其功能。就是使用對象時,只關注對象提供的功能,不關注其內部細節。典型的應用實例就是 jQuery。

  面向對象是一種通用的思想,並非只有編程中能用,任何事情都可以使用,生活中充滿了面向對象的思想,只是我們不直接叫面向對象,而是叫一些別的什麼。比如你去喫飯,你就告訴廚師來一份紅燒肉,然後就可以坐下來等着吃了,你不可能給廚師說要把肉切成方的或者圓的,要先放鹽,再放醬油,還要加紅糖,加冰糖也可以,誰真要這樣,廚師非得跟你急,他是廚師還是你是廚師,你只要把想喫的告訴他,你不用去管他是怎麼做的,他自然會做好給你端上來,這就是生活中典型的面向對象的思想。

  雖然不同於傳統的面向對象編程語言,但是 JS 也有很強的面向對象編程能力,接下來就具體分析以下什麼是 JS 面向對象編程。

  面向對象編程(Object Oriented Programming,縮寫爲OOP)是目前主流的編程範式,所謂範式,就是符合某一種級別的關係模式的集合。他的核心思想是將真實世界中各種複雜的關係,抽象爲一個個對象,然後由對象之間的分工與合作,完成對真實世界的模擬。面向對象編程的程序就是符合某一種級別的關係模式的集合,是一系列對象的組合,每一個對象都是功能中心,具有明確分工,可以完成接受信息、處理數據、發出信息等任務。因此,面向對象編程具有靈活性、代碼的可重用性、模塊性等特點,並且容易維護和開發,非常適合多人合作的大型項目,而在平時項目中一般不常使用。

  面向對象編程(OOP)的特點:

  (1)、抽象:抓住核心問題

  所謂抽象,先來看看百度對於抽象的解釋:抽象是從衆多的事物中抽取出共同的、本質性的特徵,而捨棄其非本質的特徵。例如蘋果、香蕉、鴨梨、葡萄、桃子等,它們共同的特性就是水果。得出水果概念的過程,就是一個抽象的過程。要抽象,就必須進行比較,沒有比較就無法找到在本質上共同的部分。共同特徵是指那些能把一類事物與其他類事物區分開來的特徵,這些具有區分作用的特徵又稱本質特徵。因此抽取事物的共同特徵就是抽取事物的本質特徵,捨棄非本質的特徵。所以抽象的過程也是一個裁剪的過程。在抽象時,同與不同,決定於從什麼角度上來抽象。抽象的角度取決於分析問題的目的。

  在 JS 中,抽象的核心就是抽,就是抓住共同特徵,抓住核心的問題。比如說人,有很多特徵,比如姓名、性別、籍貫、出生日期、身高、體重、血型、家庭住址、父母是誰、孩子叫啥等,如果一個公司要建立員工檔案,不可能將每個特徵都註明,需要抓住一些主要的特徵,比如:姓名、性別、部門、職位,或者再加上入職日期就完了。如果需要註冊一個婚戀網站,那這時候就不是需要員工檔案中註明的那些特點了,要一些比如:性別、年齡、身高、體形、星座、有車否、有房否、工作、收入、家庭狀況等。就是把一類事物主要的特徵、跟問題相關的特徵抽取出來。這就是面向對象編程的抽象。

  (2)、封裝:不考慮內部實現,只考慮功能使用

  何爲封裝,就好比一臺電視機,我們能看到電視機,比如外觀、顏色等,但是看不到內部的構成,我們也不用知道內部是什麼鬼,依然可以正常使用,除非這貨壞了,這內部的東西就是封裝。JS 就是不考慮內部的實現,只考慮功能的使用,就像使用 jQuery 一樣,jQuery 就是對 JS 的封裝,我們使用 jQuery 的功能,能完成與 JS 相同的效果,並且還比使用 JS 更方便。

  (3)、繼承:從已有對象上,繼承出新的對象

  所謂繼承,也可以叫做遺傳,通俗理解就是父母能幹的事孩子也能幹,比如喫飯,睡覺。在 JS 中,比如有一個對象 A,A 中有一些功能,現在從 A 中繼承出一個對象 B,這個對象 B 就具有對象 A 的所有功能。

  還有一種情況是多重繼承,好比一個孩子可以有好多個爹,顯然這是不可能的事,但是在程序中這是可行的,比如有一類盒子,盒子有一個特徵可以用來裝東西,還有一類汽車,汽車的特徵就是會跑,有軲轆,這時候就可以多重繼承,繼承出另一類集裝箱貨車,他特徵既可以裝東西又會跑,有軲轆。

  (4)、多態

  多態,顧名思義就是多種狀態,在面嚮對象語言中,接口的多種不同的實現方式即爲多態。多態在 JS 中不是那麼明顯,但是對於強語言比較有用,比如 Java,C++。對於 JS 這種弱語言,意義並不大。

 

2、對象的組成

  對象可分爲宿主對象,本地對象和內置對象。

  宿主對象就是 DOM 和 BOM,即由瀏覽器提供的對象。

  本地對象爲非靜態對象,所謂本地對象,就是需要先 new,再使用。常用的對象如:Object、Function、Array、String、Boolean、Number、Date、RegExp、Error。

  內置對象爲靜態對象,就是不需要 new,直接可以使用的類。Math 是最常見,也是可以直接使用的僅有的內置對象。

  面向對象的第一步,就是要創建對象。典型的面向對象編程的語言都存在 "類"(class) 這樣一個概念,所謂類,就是對象的抽象,表示某一類事物的共同特徵,比如水果,而對象就是類的具體實例,比如蘋果就是水果的一種,類是抽象的,不佔用內存,而對象是具體的,佔用存儲空間。但是在 JS 中沒有 "類" 這個概念,不過可以使用構造函數實現。

  之前我們說過,所謂"構造函數",就是用來創建新對象的函數,作爲對象的基本結構,一個構造函數,可以創建多個對象,這些對象都有相同的結構。構造函數就是一個普通的函數,但是他的特徵與用法和普通函數不一樣。構造函數的最大特點就是,在創建對象時必須使用 new 關鍵字,並且函數體內部可以使用 this 關鍵字,代表了所要創建的對象實例,this 就用於指向函數執行時的當前對象。具體情況下面我們再做分析,現在先來研究下對象的組成。

  其實我們已經理解了對象的概念,也就不難看出他是由什麼構成的,對象就是由屬性和方法組成的,JS 中一切皆對象,那在 JS 中,屬性和方法到底該怎麼理解呢?屬性就是變量,方法就是函數。屬性代表狀態,就像動物的屬性記錄他具體是哪一種動物一樣,他是靜態的,變量名稱也可以說是方法名稱,是對方法的描述。而方法也就是行爲,是完成某種任務的過程,他是動態的。

 

3、面向對象編程

  我們通過實例的方式,爲對象添加屬性和方法,來理解對象的組成和麪向對象。

  (1)、實例:給對象添加屬性

 <script>
 var a = 2;
 alert(a);    //返回:2
 
 var arr = [5,6,7,8,9];
 //給數組定義一個屬性a,等於2。
 arr.a = 2;
 alert(arr.a);    //返回:2
 arr.a++;
 alert(arr.a);    //返回:3
 </script>

 

  通過上面的實例,我們可以看到,變量和屬性就是一樣的,變量可以做的事,屬性也可以做,屬性可以做的事,變量也可以做。他們的區別就在於,變量是自由的,不屬於任何對象,而屬性不是自由的,他是屬於一個對象的,就像例子中的對象 a,他是屬於數組 arr 的,在使用的時候就寫爲 arr.a。我們可以給任何對象定義屬性,比如給 DIV 定義一個屬性用於索引:oDiv[i].index = i。

  (2)、實例:給對象添加方法

 <script>
 function a(){
     alert('abc');    //返回:abc
 }
 
 var arr = [5,6,7,8,9];
 //給函數添加一個a函數的方法
 arr.a = function (){
     alert('abc');    //返回:abc
 };
 a();
 arr.a();
 </script>

  通過上面的實例,可以看到,a 函數也是自由的,而當這個 a 函數屬於一個對象的時候,這就是方法,是數組 arr 的 a 方法,也就是這個對象的方法。所以函數和方法也是等同的,函數可以做的事,方法就可以做,他們的不同,也是在於函數是自由的,而方法是屬於一個對象的。

  我們不能在系統對象中隨意附加屬性和方法,否則會覆蓋已有的屬性和方法。比如實例中我們是在數組對象上附加屬性和方法的,數組有他自己的屬性和方法,我們再給其附加屬性和方法,就會覆蓋掉數組本身的屬性和方法,這一點需要注意。

  (3)、實例:創建對象

 <script>
 var obj = new Object();
 var d = new Date();
 var arr = new Array();
 alert(obj);    //返回:[object Object]
 alert(d);    //返回當前時間
 alert(arr);    //返回爲空,空數組
 </script>

  創建一個新對象,可以 new 一個 Object。object 是一個空白對象,只有系統自帶的一些很少量的東西,所以在實現面向對象的時候,就可以給 object 上加方法,加屬性。這樣可以最大限度的避免跟其他起衝突。

  (4)、實例:面向對象程序

 <script>
 //創建一個對象
 var obj = new Object();
 //可寫爲:var obj={};
 
 //給對象添加屬性
 obj.name = '小白';
 obj.qq = '89898989';
 
 //給對象添加方法
 obj.showName = function (){
     alert('我的名字叫:'+this.name);
 };
 obj.showQQ = function (){
     alert('我的QQ是:'+this.qq);
 };
 obj.showName();    //返回:我的名字叫:小白
 obj.showQQ();    //返回:我的QQ是:89898989
 
 //再創建一個對象
 var obj2 = new Object();
 
 obj2.name = '小明';
 obj2.qq = '12345678';
 
 obj2.showName = function (){
     alert('我的名字叫:'+this.name);
 };
 obj2.showQQ = function (){
     alert('我的QQ是:'+this.qq);
 };
 obj2.showName();    //返回:我的名字叫:小白
 obj2.showQQ();        //返回:我的QQ是:12345678
 </script>

  這就是一個最簡單的面向對象編程,創建一個對象,給對象添加屬性和方法,模擬現實情況,針對對象進行編程。這個小程序運行是沒有什麼問題,但是存在很嚴重的缺陷,一個網站中不可能只有一個用戶對象,可能有成千上萬個,不可能給每個用戶都 new 一個 object。其實可以將其封裝爲一個函數,然後再調用,有多少個用戶,調用多少次,這樣的函數就被稱爲構造函數。

 

4、構造函數

  構造函數(英文:constructor)就是一個普通的函數,沒什麼區別,但是爲什麼要叫"構造"函數呢?並不是這個函數有什麼特別,而是這個函數的功能有一些特別,跟別的函數就不一樣,那就是構造函數可以構建一個類。構造函數的方式也可以叫做工廠模式,因爲構造函數的工作方式和工廠的工作方式是一樣的。工廠模式又是怎樣的呢?這個也不難理解,首先需要原料,然後就是對原料進行加工,最後出廠,這就完事了。構造函數也是同樣的方式,先創建一個對象,再添加屬性和方法,最後返回。既然說構造函數可以構建一個類出來,這個該怎麼理解呢?很 easy,可以用工廠方式理解,類就相當於工廠中的模具,也可以叫模板,而對象就是零件、產品或者叫成品,類本身不具備實際的功能,僅僅只是用來生產產品的,而對象才具備實際的功能。比如:var arr = new Array(1,2,3,4,5); Array 就是類,arr 就是對象, 類 Array 沒有實際的功能,就是用來存放數據的,而對象 arr 具有實際功能,比如:排序sort()、刪除shift()、添加push()等。我們不可能這麼寫:new arr(); 或 Array.push();,正確的寫法:arr.push();。

 <script>
 function userInfo(name, qq){
 
     //1.原料 - 創建對象
     var obj = new Object();
 
     //2.加工 - 添加屬性和方法
     obj.name = name;
     obj.qq = qq;
     obj.showName = function (){
         alert('我的名字叫:' + this.name);
     };
     obj.showQQ = function (){
         alert('我的QQ是:' + this.qq);
     };
     //3.出廠 - 返回
     return obj;
 }
 
 var obj1 = userInfo('小白', '89898989');
 obj1.showName();
 obj1.showQQ();
 
 var obj2 = userInfo('小明', '12345678');
 obj2.showName();
 obj2.showQQ();
 </script>

 

  這個函數的功能就是構建一個對象,userInfo() 就是構造函數,構造函數作爲對象的類,提供一個模具,用來生產用戶對象,我們以後在使用時,只調用這個模板,就可以無限創建用戶對象。我們都知道,函數如果用於創建新的對象,就稱之爲對象的構造函數,我們還知道,在創建新對象時必須使用 new 關鍵字,但是上面的代碼,userInfo() 構造函數在使用時並沒有使用 new關 鍵字,這是爲什麼呢?且看下文分解。

 

5、new 和 this

  (1)new

  new 關鍵字的作用,就是執行構造函數,返回一個實例對象。看下面例子:

<script>
var user = function (){
  this.name = '小明';
};

var info = new user();
alert(info.name);    //返回:小明
</script>

 

   上面實例通過 new 關鍵字,讓構造函數 user 生產一個實例對象,保存在變量 info 中,這個新創建的實例對象,從構造函數 user 繼承了 name 屬性。在 new 命令執行時,構造函數內部的 this,就代表了新生產的實例對象,this.name 表示實例有一個 name 屬性,他的值是小明。

  使用 new 命令時,根據需要,構造函數也可以接受參數。

 <script>
 var user = function (n){
   this.name = n;
 };
 
 var info = new user('小明');
 alert(info.name);    //返回:小明
 </script>

 

  new 命令本身就可以執行執行構造函數,所以後面的構造函數可以帶括號,也可以不帶括號,下面兩行代碼是等價的。

var info = new user;
var info = new user();

  那如果沒有使用 new 命令,直接調用構造函數會怎樣呢?這種情況下,構造函數就變成了普通函數,並不會生產實例對象,this 這時候就代表全局對象。

 <script>
 var user = function (n){
   this.name = n;
 };
 alert(this.name);    //返回:小明
 
 var info = user('小明');
 alert(info.name);    //報錯
 </script>

  上面實例中,調用 user 構造函數時,沒有使用 new 命令,結果 name 變成了全局變量,而變量 info 就變成了 undefined,報錯:無法讀取未定義的屬性 'name'。使用 new 命令時,他後邊的函數調用就不是正常的調用,而是被 new 命令控制了,內部的流程是,先創建一個空對象,賦值給函數內部的 this 關鍵字,this 就指向一個新創建的空對象,所有針對 this 的操作,都會發生在這個空對象上,構造函數之所以叫"構造函數",就是說這個函數的目的,可以操作 this 對象,將其構造爲需要的樣子。下面我們看一下 new 和函數。

 <script>
 var user = function (){
 //function = user(){
     alert(this);
 }
 user();    //返回:Window
 new user();//返回:Object
 </script>

 

  通過上面實例,可以看到,在調用函數時,前邊加個 new,構造函數內部的 this 就不是指向 window 了,而是指向一個新創建出來的空白對象。

  說了這麼多,那爲什麼我們第四章的構造函數,在使用的時候沒有加 new 關鍵字呢,因爲我們完全是按照工廠模式,也就是構造函數的結構直接編寫的,我們的步驟已經完成了 new 關鍵字的使命,也就是把本來 new 需要做的事,我們已經做了,所以就用不着 new 了。那這樣豈不是做了很多無用功,寫了不必要的代碼,浪費資源,那肯定是了,這也是構造函數的一個小問題,我們在下一章再做具體分析。

  (2)、this

  this 翻譯爲中文就是這,這個,表示指向。之前我們提到過,this 指向函數執行時的當前對象。那麼我們先來看看函數調用,函數有四種調用方式,每種方式的不同方式,就在於 this 的初始化。

  ①、作爲一個函數調用

 <script>
 function show(a, b) {
     return a * b;
 }
 alert(show(2, 3));    //返回:6
 </script>

 

  實例中的函數不屬於任何對象,但是在 JS 中他始終是默認的全局對象,在 HTML 中默認的全局對象是 HTML 頁面本身,所以函數是屬於 HTML 頁面,在瀏覽器中的頁面對象是瀏覽器窗口(window 對象),所以該函數會自動變爲 window 對象的函數。

 <script>
 function show(a, b) {
     return a * b;
 }
 alert(show(2, 3));    //返回:6
 alert(window.show(2, 3));//返回:6
 </script>

  上面代碼中,可以看到,show() 和 window.show() 是等價的。這是調用 JS 函數最常用的方法,但不是良好的編程習慣,因爲全局變量,方法或函數容易造成命名衝突的 Bug。

  當函數沒有被自身的對象調用時,this 的值就會變成全局對象。

 <script>
 function show() {
     return this;
 }
 alert(show());    //返回:[object Window]
 </script>

  全局對象就是 window 對象,函數作爲全局對象對象調用,this 的值也會成爲全局對象,這裏需要注意,使用 window 對象作爲一個變量容易造成程序崩潰。

  ②、函數作爲方法調用

 <script>
 var user = {
     name : '小明',
     qq : 12345678,
     info : function (){
         return this.name + 'QQ是:' + this.qq;
     }
 }
 alert(user.info());
 </script>

  在 JS 中可以將函數定義爲對象的方法,上面實例創建了一個對象 user,對象擁有兩個屬性(name和qq),及一個方法 info,該方法是一個函數,函數屬於對象,user 是函數的所有者,this 對象擁有 JS 代碼,實例中 this 的值爲 user 對象,看下面示例:

 <script>
 var user = {
     name : '小明',
     qq : 12345678,
     info : function (){
         return this;
     }
 }
 alert(user.info());    //返回:[object Object]
 </script>

  函數作爲對象方法調用,this 就指向對象本身。

  ③、使用構造函數調用函數

  如果函數調用前使用了 new關鍵字,就是調用了構造函數。

 <script>
 function user(n, q){
     this.name = n;
     this.qq  = q;
 }
 
 var info = new user('小明', 12345678);
 alert(info.name);    //返回:小明
 alert(info.qq);        //返回:12345678
 </script>

  這看起來就像創建了新的函數,但實際上 JS 函數是新創建的對象,構造函數的調用就會創建一個新的對象,新對象會繼承構造函數的屬性和方法。構造函數中的 this 並沒有任何的值,this 的值在函數調用時實例化對象(new object)時創建,也就是指向一個新創建的空白對象。

  ④、作爲方法函數調用函數

  在 JS 中,函數是對象,對象有他的屬性和方法。call() 和 apply() 是預定義的函數方法,這兩個方法可用於調用函數,而且這兩個方法的第一個參數都必須爲對象本身。

 <script>
 function show(a, b) {
     return a * b;
 }
 var x = show.call(show, 2, 3);
 alert(x);    //返回:6
 
 function shows(a, b) {
     return a * b;
 }
 var arr = [2,3];
 var y = shows.apply(shows, arr);
 var y1 = shows.call(shows, arr);
 alert(y);    //返回:6
 alert(y1);    //返回:NaN
 </script>

  上面代碼中的兩個方法都使用了對象本身作爲作爲第一個參數,兩者的區別在於:apply()方法傳入的是一個參數數組,也就是將多個參數組合稱爲一個數組傳入,而call()方法則作爲call的參數傳入(從第二個參數開始),不能傳入一個參數數組。

  通過 call() 或 apply() 方法可以設置 this 的值, 且作爲已存在對象的新方法調用。在下面用到的時候,我們再具體分析。

  this 就是用於指向函數執行時的當前對象,下面再看一個實例:

 <body>
 <div id="div1"></div>
 <script>
 var oDiv = document.getElementById('div1');
 //給一個對象添加事件,本質上是給這個對象添加方法。
 oDiv.onclick = function (){
     alert(this);    //this就是oDiv
 };
 
 var arr = [1,2,3,4,5];
 //給數組添加屬性
 arr.a = 12;
 //給數組添加方法
 arr.show = function (){
     alert(this.a);    //this就是arr
 };
 arr.show();    //返回:12
 
 
 function shows(){
     alert(this);    //this就是window
 }
 
 //全局函數是屬於window的。
 //所以寫一個全局函數shows和給window加一個shows方法是一樣的。
 window.shows = function (){
     alert(this);
 };
 shows();    //返回:[object Window]
 </script>
 </body>

   上面的代碼,this 就代表着當前的函數(方法)屬於誰,如果是一個事件方法,this 就是當前發生事件的對象,如果是一個數組方法,this 就是數組對象,全局的方法是屬於 window 的,所以 this 指向 window。

 

6、原型

  前面我們說過構造函數在使用時沒有加 new,這隻能算是一個小問題,沒有加我們可以給加上,無傷大雅,但其實他還存在着一個更嚴重的問題,那就是函數重複定義。

 <script>
 function userInfo(name, qq){
 
     //1.原料 - 創建對象
     var obj = new Object();
 
     //2.加工 - 添加屬性和方法
     obj.name = name;
     obj.qq = qq;
     obj.showName = function (){
         alert('我的名字叫:'+this.name);
     };
     obj.showQQ = function (){
         alert('我的QQ是:'+this.qq);
     };
     //3.出廠 - 返回
     return obj;
 }
 
 //1.沒有new。
 var obj1 = userInfo('小白', '89898989');
 var obj2 = userInfo('小明', '1234567');
 
 //調用的showName返回的函數都是相同的。
 alert(obj1.showName);
 alert(obj2.showName);
 
 //2.函數重複。
 alert(obj1.showName == obj2.showName);    //返回:false
 </script>

  通過上面的代碼,我們可以看到,彈出這兩個對象的 showName,調用的 showName 返回的函數是相同的,他們新創建對象所使用的方法都是一樣的,儘管這兩個函數長的是一樣的,但其實他們並不是一個東西,我們將 對象1 和 對象2 做相等比較,結果返回 false。這時候就帶來了一個相對嚴重的問題,一個網站中也不可能只有 2 個用戶,比如有 1 萬個用戶對象,那麼就會有 1 萬 showName 和 showQQ 方法,每一個對象都有自己的函數,但明明這兩個函數都是一樣的,結果卻並非如此。這樣就很浪費系統資源,而且性能低,可能還會出現一些意想不到的問題。該怎麼解決這個問題呢?方法也很簡單,就是使用原型。

  (1)、什麼是原型

  JS 對象都有一個之前我們沒有講過的屬性,即 prototype 屬性,該屬性讓我們有能力向對象添加屬性和方法,包括 String對象、Array對象、Number對象、Date對象、Boolean對象,Math對象 並不像 String 和 Date 那樣是對象的類,因此沒有構造函數 Math(),該對象只用於執行數學任務。

  所有 JS 的函數都有一個prototype屬性,這個屬性引用了一個對象,即原型對象,也簡稱原型。這個函數包括構造函數和普通函數,我們講的更多是構造函數的原型,但是也不能否定普通函數也是有原型的。

  在看實例之前,我們先來看幾個小東西:typeof運算符、constructor屬性、instanceof運算符。

  typeof 大家都熟悉,JS 中判斷一個變量的數據類型就會用到 typeof 運算符,返回結果爲 JS 基本的數據類型,包括 number、string、boolean、object、function、undefined,語法:typeof obj。

  constructor 屬性返回所有 JS 變量的構造函數,typeof 無法判斷 Array對象 和 Date對象 的類型,因爲都返回 object,所以我們可以利用 constructor 屬性來查看對象是否爲數組或者日期,語法:obj.constructor。

 <script>
 var arr = [1,2,3,4,5];
 function isArray(obj) {
     return arr.constructor.toString().indexOf("Array") > -1;
 }
 alert(isArray(arr));    //返回:ture
 
 var d = new Date();
 function isDate(obj) {
     return d.constructor.toString().indexOf("Date") > -1;
 }
 alert(isDate(d));    //返回:ture
 </script>

   這裏需要注意,constructor 只能對已有變量進行判斷,對於未聲明的變量進行判斷會報錯,而 typeof 則可對未聲明變量進行判斷(返回undefined)。

  instanceof 這東西比較高級,可用於判斷一個對象是否是某一種數據類型,查看對象是否是某個類的實例,返回值爲 boolean 類型。另外,更重要的一點是 instanceof 還可以在繼承關係中用來判斷一個實例是否屬於他的父類型,語法:a instanceof b。

 <script>
 // 判斷 a 是否是 A 類的實例 , 並且是否是其父類型的實例
 function A(){} 
 function B(){} 
 B.prototype = new A();    //JS原型繼承
 
 var a = new B();
 alert(a instanceof A);    //返回:true
 alert(a instanceof B);    //返回:true 
 </script>

   上面的實例中判斷了一層繼承關係中的父類,在多層繼承關係中,instanceof 運算符同樣適用。

  下面我們就來看看普通函數的原型:

 <script>
 function A(){}
 alert(A.prototype instanceof Object);    //返回:true
 </script>

  上面代碼中 A 是一個普通的函數,我們判斷函數 A 的原型是否是對象,結果返回 true。

  說了這麼多,原型到底是個什麼東西,說簡單點原型就是往類的上面添加方法,類似於class,修改他可以影響一類元素。原型就是在已有對象中加入自己的屬性和方法,原型修改已有對象的影響,prototype屬性可返回對象類型原型的引用,如果對象創建在修改原型之前,那麼該對象不會擁有修改後的原型方法,就是說原型鏈的改變,不會影響之前產生的對象。有關原型鏈的知識,下面我們在講繼承時,再做分析。

  下面我們通過實例的方式,進一步的理解原型。

  實例:給數組添加方法

 <script>
 var arr1 = new Array(2,8,8);
 var arr2 = new Array(5,5,10);
 
 arr1.sum = function (){
     var result = 0;
     for(var i=0; i<this.length; i++){
         result += this[i];
     }
     return result;
 };
 
 alert(arr1.sum());  //返回:18
 alert(arr2.sum());  //報錯:arr2沒有sum方法
 </script>

  上面的實例只給 數組1 添加了 sum 方法,這就類似於行間樣式,只給 arr1 設置了,所以 arr2 肯定會報錯,這個並不難理解。

  實例:給原型添加方法

 <script>
 var arr1 = new Array(2,8,8);
 var arr2 = new Array(5,5,10);
 
 Array.prototype.sum = function (){
     var result = 0;
     for(var i=0; i<this.length; i++){
         result += this[i];
     }
     return result;
 };
 
 alert(arr1.sum());    //返回:18
 alert(arr2.sum());    //返回:20
 </script>

  通過上面的實例,我們可以看到,通過原型 prototype 給 Array 這個類添加一個 sum 方法,就類似於 class,一次可以設置一組元素,那麼所有的 Array 類都具有這個方法,arr1 返回結果爲 18,而 arr2 在加了原型之後,也返回了正確的計算結果 20。

  (2)、解決歷史遺留問題

  現在我們就可以使用原型,來解決沒有 new 和函數重複定義的問題了。

 <script>
 function UserInfo(name, qq){
 
     //1.原料 - 創建對象
     //var obj = new Object();
 
     //加了new之後,系統(瀏覽器)會自動替你聲明一個變量:
     //var this = new Object();
 
     //2.加工 - 添加屬性和方法
     /*
     obj.name = name;
     obj.qq = qq;
     obj.showName = function (){
         alert('我的名字叫:'+this.name);
     };
     obj.showQQ = function (){
         alert('我的QQ是:'+this.qq);
     };
     */
     this.name = name;
     this.qq = qq;
 
     //3.出廠 - 返回
     //return obj;
 
     //系統也會自動替你返回:
     //return this;
 }
 
 //2.函數重複的解決:userInfo給類加原型。
 UserInfo.prototype.showName = function (){
     alert('我的名字叫:' + this.name);
 };
 
 UserInfo.prototype.showQQ = function (){
     alert('我的QQ是:' + this.qq);
 };
 
 
 //1.加上沒有new。
 var obj1 = new UserInfo('小白', '89898989');
 var obj2 = new UserInfo('小明', '1234567');
 
 obj1.showName();
 obj1.showQQ();
 obj2.showName();
 obj2.showQQ();
 
 //加了原型之後
 alert(obj1.showName == obj2.showName);    //返回:true
 </script>

 

  上面的代碼看着有點複雜,我們把不必要的省略,如下:

 <script>
 function UserInfo(name, qq){
     this.name = name;
     this.qq = qq;
 }
 
 UserInfo.prototype.showName = function (){
     alert('我的名字叫:' + this.name);
 };
 UserInfo.prototype.showQQ = function (){
     alert('我的QQ是:' + this.qq);
 };
 
 var obj1 = new UserInfo('小白', '89898989');
 var obj2 = new UserInfo('小明', '1234567');
 
 obj1.showName();
 obj1.showQQ();
 obj2.showName();
 obj2.showQQ();
 
 alert(obj1.showName == obj2.showName);    //返回:true
 </script>

  現在代碼是不是比最初的樣子,簡潔了很多,new 關鍵字也使用了,而且每個對象都是相等的。通過上面的實例,我們可以看到,再加上 new 之後,使用就方便了很多,代碼明顯減少了,因爲在加了 new 之後,系統也就是瀏覽器自動爲你做兩件事,這就是 new 的使命,第一件事是替你創建了一個空白對象,也就是替你聲明瞭一個變量:var this = new Object();,第二件事就是再提你返回這個對象:return this;,這裏需要注意,在之前我們也講過,在調用函數的時候,前邊加個 new,構造函數內部的 this 就不是指向 window 了,而是指向一個新創建出來的空白對象。

  這種方式就是流行的面向對象編寫方式,即混合方式構造函數,混合的構造函數/原型方式(Mixed Constructor Function/Prototype Method),他的原則是:用構造函數加屬性,用原型加方法,也就是用構造函數定義對象的所有非函數屬性,用原型方式定義對象的函數方法。用原型的作用,就是此對象的所有實例共享原型定義的數據和(對象)引用,防止重複創建函數,浪費內存。原型中定義的所有函數和引用的對象都只創建一次,構造函數中的方法則會隨着實例的創建重複創建(如果有對象或方法的話)。這裏需要注意,不管在原型中還是構造函數中,屬性(值)都不共享,構造函數中的屬性和方法都不共享,原型中屬性不共享,但是對象和方法共享。所以創建類的最好方式就是用構造函數定義屬性,用原型定義方法。使用該方式,類名的首字母要大寫,這也是一種對象命名的規範。

 

7、面向對象實例

  通常我們在寫程序時,都使用的是面向過程,即要呈現出什麼效果,基於這樣的效果,一步步編寫實現效果的代碼,接下來我們就把面向過程的程序,改寫成面向對象的形式。面向過程的程序寫起來相對容易些,代碼也比較直觀,易讀性強,我們先看一個面向過程的實例。

  實例:面向過程的選項卡

 <!DOCTYPE html>
 <html>
 <head>
     <meta charset="UTF-8">
     <title>JavaScript實例</title>
 <style>
 #div1 input{background:white;}
 #div1 input.active{background:green;color:white;}
 #div1 div{
     width:200px;
     height:200px;
     background:#ccc;
     display:none;
 }
 </style>
 <script>
 window.onload = function (){
     //1、獲取所需元素。
     var oDiv = document.getElementById('div1');
     var oBtn = oDiv.getElementsByTagName('input');
     var aDiv = oDiv.getElementsByTagName('div');
     
     //2、循環遍歷所有按鈕。
     for(var i=0; i<oBtn.length; i++){
         //5、給按鈕定義index屬性,當前按鈕的索引號爲按鈕的索引號i
         oBtn[i].index = i;
         //3、給當前按鈕添加點擊事件。
         oBtn[i].onclick = function (){
            //4、再循環所有按鈕,清空當前按鈕的class屬性,並將當前內容的樣式設置爲隱藏
            //在執行清空和設置之前,需要給當前按鈕定義一個索引
            //這一步的目的:主要就是實現切換效果,點擊下一個按鈕時,當前按鈕失去焦點,內容失去焦點
              for(var i=0; i<oBtn.length; i++){
                 oBtn[i].className = '';
                 aDiv[i].style.display = 'none';
             }
             //6、最後給當前按鈕class屬性,再設置當前展示內容的樣式爲顯示
             this.className = 'active';
             aDiv[this.index].style.display = 'block';
        };
     }
 };
 </script>
 </head>
 <body>
 <div id="div1">
     <input class="active" type="button" value="新聞">
     <input type="button" value="熱點">
     <input type="button" value="推薦">
     <div style="display:block;">天氣預報</div>
     <div>歷史實事</div>
     <div>人文地理</div>
 </div>
 </body>
 </html>

 

  這樣一個簡單的效果,誰都可以做的出來,那要怎麼寫成面向對象的形式呢,我們先來看代碼,再做分析。

  實例:面向對象的選項卡

 <!DOCTYPE html>
 <html>
 <head>
     <meta charset="UTF-8">
     <title>JavaScript實例</title>
 <style>
 #div1 input{background:white;}
 #div1 input.active{background:green;color:white;}
 #div1 div{
     width:200px;
     height:200px;
     background:#ccc;
     display:none;
 }
 </style>
 <script>
 window.onload = function(){
     new TabShow('div1');
 };
 
 function TabShow(id){
     var _this = this;
     var oDiv = document.getElementById(id);
     this.oBtn = oDiv.getElementsByTagName('input');
     this.aDiv = oDiv.getElementsByTagName('div');
     for(var i=0; i<this.oBtn.length; i++){
         this.oBtn[i].index = i;
         this.oBtn[i].onclick = function (){
             _this.fnClick(this);
         };
     }
 }
 
 TabShow.prototype.fnClick = function (oBtn){
     for(var i=0; i<this.oBtn.length; i++){
         this.oBtn[i].className = '';
         this.aDiv[i].style.display = 'none';
     }
     oBtn.className = 'active';
     this.aDiv[oBtn.index].style.display = 'block';
 };
 </script>
 </head>
 <body>
 <div id="div1">
     <input class="active" type="button" value="新聞">
     <input type="button" value="熱點">
     <input type="button" value="推薦">
     <div style="display:block;">天氣預報</div>
     <div>歷史實事</div>
     <div>人文地理</div>
 </div>
 </body>
 </html>

   將面向過程的程序,改寫成面向對象的形式,原則就是不能有函數套函數,但可以有全局變量,其過程是先將 onload 改爲構造函數,再將全局變量改爲屬性,函數改爲方法,這就是面向對象的思維,所以第一步就是把嵌套函數單獨出來,當函數單獨出去之後,onload 中定義的變量在點擊函數中就會報錯,onload 也相當於一個構造函數,初始化整個程序,所以再對 onload 函數作出一些修改,讓他初始化這個對象,然後就是添加屬性和方法,我們說變量就是屬性,函數就是方法,所以這裏也只是改變所屬關係。這個過程中最需要注意的是 this 的指向問題,通過閉包傳遞 this,以及函數傳參,把對象作爲參數傳遞。之前的 this 都是指向當前發生事件的對象,將函數改爲方法後,我們給這個方法添加的是按鈕點擊事件,所以這時候 this 就指向這個按鈕,本應該這個 this 是指向新創建的對象,這就需要轉換 this 的指向 var _this = this;。TabShow 函數就是 onload 函數的改造,fnClick 方法是第一步單獨出去的函數,最後被改爲了選項卡函數 (TabShow函數) 的方法。

 

8、繼承和原型鏈

  (1)、繼承

  前邊我們簡單的說過繼承是從已有對象上,再繼承出一個新對象,繼承就是在原有類的基礎上,略作修改,得到一個新的類,不影響原有類的功能。繼承的實現有好幾種方法,最常用的就是 call() 方法和原型實現繼承。下面看一個繼承的實例:

 <script>
 function A(){
     this.abc = 12;
 }
 A.prototype.show = function (){
     alert(this.abc);
 };
 
 function B(){
     A.call(this);
 }
 
 for(var i in A.prototype){
     B.prototype[i]=A.prototype[i];
 }
 
 B.prototype.fn=function (){
     alert('abc');
 };
 
 var objB = new B();
 alert(objB.abc);    //返回:12
 objB.show();        //返回:12
 objB.fn();            //返回:abc
 
 var objA = new A();
 objA.fn();    //報錯:A沒有該方法
 </script>

  上面的代碼,B函數 繼承了 A函數 的屬性,通過 call 方法,該方法有一個功能,可以改變這個函數在執行時裏邊的 this 的指向,如果 B函數 中不使用 call,this 則指向 new B(),使用 call 後,this 則指向 A。方法繼承 B.prototype = A.prototype;,A 的方法寫在原型裏,賦給 原型B,原型也是引用,將 A的原型 引用給 B的原型,就相當於 原型A 和 原型B 公用引用一個空間,所以 原型B 自己的方法,原型A 也可以用,給 原型B 添加一個方法,也就是給 原型A 添加一個方法。所以可以使用循環遍歷 原型A 中的內容,再將這些內容賦給 原型B,這樣 原型A 就沒有 原型B 的方法了,也就是給 B 再添加方法,A 將不會受到影響(objA.fn() 報錯),B 不僅有從父級繼承來的方法(objB.show()),還有自己的方法(obj.fn())。

  (2)、原型鏈

  在 JS 中,每當定義一個對象(函數)時,對象中都會包含一些預定義的屬性。其中函數對象的一個屬性就是原型對象 prototype。這裏需要注意:普通對象沒有 prototype,但有__proto__ 屬性。原型對象的主要對象就是用於繼承。

 <script>
 var A = function(name){
     this.name = name;
 };
 A.prototype.getName = function(){
     alert(this.name); 
 }
 var obj = new A('小明');
 obj.getName();    //返回:小明
 
 </script>

   上面的代碼,通過給 A.prototype 定義了一個函數對象的屬性,再 new 出來的對象就繼承了這個屬性。

  JS 在創建對象(不論是普通對象還是函數對象)時,都有一個叫做 __proto__ 的內置屬性,用於指向創建它的函數對象的原型對象 prototype。

 <script>
 var A = function(name){
     this.name = name;
 }
 A.prototype.getName = function(){
     alert(this.name); 
 }
 var obj = new A('小明');
 obj.getName();    //返回:小明
 
 alert(obj.__proto__ === A.prototype);    //返回:true
 </script>

  同樣,A.prototype 對象也有 __proto__ 屬性,它指向創建它的函數對象(Object)的 prototype。

 <script>
 var A = function(name){
     this.name = name;
 }
 A.prototype.getName = function(){
     alert(this.name); 
 }
 var obj = new A('小明');
 obj.getName();        //返回:小明
 
 alert(A.prototype.__proto__ === Object.prototype);    //返回:true
 </script>

  Object.prototype 對象也有 __proto__ 屬性,但它比較特殊,爲 null。

 <script>
 var A = function(name){
     this.name = name;
 }
 A.prototype.getName = function(){
     alert(this.name); 
 }
 var obj = new A('小明');
 obj.getName();        //返回:小明
 
 alert(Object.prototype.__proto__);    //返回:null
 </script>

  綜上,我們把這個由 __proto__ 串起來的直到 Object.prototype.__proto__ 爲 null 的鏈就叫做原型鏈。

  在 JS 中,可以簡單的將值分爲兩種類型,即原始值和對象值。每個對象都有一個內部屬性 (prototype),通常稱之爲原型。原型的值可以是一個對象,也可以是 null。如果他的值是一個對象,則這個對象也一定有自己的原型,由於原型對象本身也是對象,而他自己的原型對象又可以有自己的原型,這樣就組成了一條鏈,我們就稱之爲原型鏈。JS 引擎在訪問對象的屬性時,如果在對象本身中沒有找到,則會去原型鏈中查找,如果找到,直接返回值,如果整個鏈都遍歷且沒有找到屬性,則返回 undefined。原型鏈一般實現爲一個鏈表,這樣就可以按照一定的順序來查找,如果對象沒有顯式的聲明自己的 ”__proto__”屬性,那麼這個值默認的設置爲 Object.prototype,而當 Object.prototype 的 ”__proto__”屬性值爲 ”null”時,則標誌着原型鏈的終結。

 

9、JSON 的面向對象

  JSON 的面向對象,就是把方法包含在一個 JSON 中,在僅僅只有一個對象時使用,整個程序只有一個對象,寫起來比較簡單,但是不適合多個對象。這種方式也被稱爲命名空間所謂命名空間,就是把很多 JSON 用附加屬性的方式創建,然後每個裏邊都有自己的方法,這種方法主要用來分類,使用方便,避免衝突。就相當於把同一類方法歸納在一起,既可以不衝突,而且找起來方便。

 <script>
 //創建一個空的json
 var json = {};
 
 //現在就有了3個空的json
 json.a = {};
 json.b = {};
 json.c = {};
 
 //現在3個json裏邊各有一個getUser函數,而且各不相同。
 //在JS中,如果是相同命名的函數就會產生衝突,相互覆蓋。
 //但是這3個json不會相互衝突,相互覆蓋。
 json.a.getUser = function (){
     alert('a');
 };
 json.b.getUser = function (){
     alert('b');
 };
 json.c.getUser = function (){
     alert('c');
 };
 json.a.getUser();    //返回:a
 json.b.getUser();    //返回:b
 json.c.getUser();    //返回:c
 </script>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章