在一般的程式語言中,我們使用繼承來複用程式碼,做成良好的資料結構。而在JavaScript中,我們使用原型來實現以上的需求。由於JavaScript專注於物件而摒棄了類,我們要明白原型和繼承的確是有差異的,但很多人接受不了這個事實,因此用某些語法來模仿類的操作。但如果我們要學習JavaScript,還是要拋開各種寫法,先從原理上理解原型。JavaScript中原型並不是一個很難的事,但它是一個全新的概念,因此需要一些時間去接受。
1. 原型在哪兒
原型指明瞭一個物件的“身份”。在前面的介紹中我們知道,一個物件被建立完成時會自動生成一個__proto__屬性,一個函式被建立完成時會自動生成一個prototype屬性,這就是我們所說的原型。所以原型本質上就是物件中的一個不可列舉的屬性,值為一個方法可以被我們這個物件共用的物件,只是JavaScript會幫助我們維護這個屬性(當然我們也可以改變原型的值)。我們期望被複用的屬性和方法都可以放入原型中,而僅自身才有的屬性就可以放入構造器裡。就像這樣:
1 var MotherLyn = function(generation, name) { 2 this.generation = generation; 3 this.name = name; 4 } 5 6 MotherLyn.prototype.show = function() { 7 return this.generation + this.name; 8 }
這樣列印出來的結構就是這個物件有兩個(generation和name)屬性,一個原型;這個原型中又有一個show方法,一個constructor屬性(值為前四行宣告建構函式的程式碼),和一個原型;這個原型就是我們的內建物件Object。由此我們可以看出,物件的原型之間形成了鏈狀的關係,這條鏈最高的端點是我們的Object物件。我們稱之為原型鏈。
2. 原型鏈
我在網上查資料的時候翻到了這張圖片,非常感激這張圖片的作者,它基本講述了一般物件、原型和構造器間的關係:
看這個圖我們基本上可以明白,Person Prototype的原型為基石,Person構造器為構造方法,由此我們來構建person例項。例項和構造器的原型都是Person Prototype原型;反過來原型中有一個構造器屬性和許多被例項繼承的屬性和方法。
我們可能會好奇例項中的[[Prototype]]和構造器中的prototype有什麼區別。[[Prototype]]這個屬性存在於物件中(像圖中就存在於person例項中),我們是無法訪問到的,只能通過瀏覽器中的__proto__屬性輔助訪問到它的值。也就是說__proto__是訪問[[Prototype]]屬性的一個方式。而prototype是函式物件中的一個屬性,只存在於函式中,可以像訪問普通屬性一樣直接訪問得到。
那我們可能又會繼續好奇了,構造器作為函式,它本身也是個物件,該怎麼辦呢。列印構造器觀察發現,實際上它既有__proto__屬性也有prototype屬性。就像人在社會上也有不止一個身份一樣,構造器一方面作為函式,另一方面自身也是一個物件。作為函式,構造器是Person類的函式,因此它的prototype屬性值為Person的原型;作為物件,構造器本身也是一個Function.prototype的子物件,因此構造器的__proto__屬性值為Function。作一個類比,林大媽這個人,在學校中是一個學生,在家庭中是一個孩子,他的原型指明瞭他的身份屬性,因此他既有學生這個原型屬性,也有孩子這個原型屬性。
對上面這個圖進行擴充套件,假如我們對Person Prototype繼續取原型,一直取,最終我們會到達Object.prototype,裡面包含了JavaScript為我們內建的許多方法,例如toString方法、hasOwnProperty方法等。我們剛才一直取原型的操作有點像在沿著一個連結串列不斷向上摸索的感覺,因此我們把它稱之為原型鏈。毫無疑問原型鏈的頂端是Object.prototype。如果再對Object.prototype取原型,會得到null,這個我們可以忽略。那麼,對於原型鏈上的每個節點,也就是每個原型,理論上說我們是可以實現擴充套件(也就是給它增加屬性或方法)的。
另外,JavaScript引擎在尋找一個物件的屬性時,會以① 先遍歷物件自身的屬性,② 找不到再遍歷原型的屬性, ③ 找不到再遍歷原型的原型的屬性, ……, 直到原型鏈的頂端Object物件,遍歷完了仍然找不到屬性時返回一個undefined。找尋方法也是如此。
3. 嘗試用原型擴充套件內建物件
當我們明白了原型是這麼一個物件時,我們就要開始想它能怎麼使用了。最淺顯的用法顯然是用來擴充套件Object,Array,Function這些內建的物件了,下面我們嘗試為Array物件擴充套件一個亂序排序的方法:
1 if(!Array.prototype.shuffle) { 2 Array.prototype.shuffle = function() { 3 for(var i = this.length, j = Math.floor(Math.random() * i), x; i; j = Math.floor(Math.random() * i)) { 4 x = this[--i]; 5 this[i] = this[j]; 6 this[j] = x; 7 } 8 return this; 9 } 10 }
或者我們要做很多擴充套件工作,覺得.prototype太長了,不想打這麼多次,也不美觀,嘗試為Function物件擴充套件函式提供一個簡單的寫法:
1 if(!Function.prototype.method) { 2 Function.prototype.method = function(name, code) { 3 if(!Function.prototype[name]) { 4 this.prototype[name] = func; 5 return this; 6 } 7 } 8 }
這樣,我們擴充套件時只需要呼叫method函式,傳入函式名和程式碼段(匿名函式也可以),就可以把函式擴充套件到物件裡面了。
4. 嘗試使用原型鏈實現繼承
假設我們有一個公司(不可能的,我不可能有公司的),要做一個非常非常非常簡單基礎的僱員管理,需求是:
① 有一個Employee僱員物件(不是例項)作為最高原型,有屬性name人名和屬性salary薪水,有方法show以字串形式展示name和salary;
② 有一個Manager經理物件(不是例項),它的原型是僱員,有屬性人名、薪水和手下陣列inferiors,還有一個方法getInferiors以字串形式展示inferiors;
③ 有一個Secretary祕書物件(不是例項),它的原型也是僱員,有屬性人名、薪水和上司superior,其中上司必為經理,還有一個方法getSuperior以字串形式展示superior。
首先建立一個僱員物件:
1 function Employee (name, salary) { 2 this.name = name; 3 this.salary = salary; 4 5 this.show = function () { 6 return this.name + ": $" + this.salary; 7 } 8 }
然後建立經理和祕書物件:
1 function Manager (name, salary, inferiors) { 2 Manager.prototype.name = name; 3 Manager.prototype.salary = salary; 4 this.inferiors = inferiors; 5 6 this.getInferiors = function () { 7 return this.inferiors; 8 } 9 } 10 11 function Secretary (name1, salary1, name2, salary2) { 12 Secretary.prototype.name = name1; 13 Secretary.prototype.salary = salary1; 14 this.superior = new Manager(name2, salary2); 15 16 this.getSuperior = function () { 17 return this.superior; 18 } 19 }
以上這些都不是關鍵,下面我們要把它們以原型形式連線起來。由於JavaScript是純基於物件的語言,它不像C++和Java一樣有“類”的概念,因此即使是原型,我們也要使用物件來表示,而不是直接連線到建構函式:
1 Manager.prototype = new Employee(); 2 Secretary.prototype = new Employee();
連線後建立manager和secretary例項,它們作為物件,__proto__屬性成功連線到了Employee例項上。
總結:① 原型是我們建立物件後JavaScript自動幫我們生成和維護的一個不可列舉物件,它指明瞭物件的“身份”,可以通過原型實現程式碼複用,但也要注意使用for in遍歷時是否要過濾掉原型上的屬性和方法;
② 不斷取一個物件的原型,最終一定會到達Object.prototype(Object.prototype的原型為null,不予討論),我們稱之為原型鏈。在JavaScript中找尋物件和屬性時會沿著原型鏈一直找,直到頂端遍歷完如果仍找不到,最終返回undefined。
③ 對於每個原型物件,包括JavaScript的內建物件,我們都可以進行程式碼的擴充套件。
④ 原型的作用類似繼承,可以幫助我們物件導向程式設計,複用程式碼。