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>