參考書籍: Javascript設計模式與開發實踐(曾探)
原型模式不單是一種設計模式,也被稱爲一種編程泛型。
原型模式 找到一個對象,然後通過 克隆 來創建一個一模一樣的對象。使用原型模式,我們只需要調用負責克隆的方法,便能完成同樣的功能。
ECMAScript 5 提供了Object.create方法,可以用來克隆對象。
但原型模式的真正目的並非在於需要得到一個一模一樣的對象,而是 提供了一種便捷的方式去創建某個類型的對象,克隆只是創建這個對象的過程和手段。
Object 是Animal 的原型,而Animal 是Dog 的原型,它們之間形成了一條原型鏈。這個原型鏈是很有用處的,當我們嘗試調用Dog 對象的某個方法時,而它本身卻沒有這個方法,那麼Dog 對象會把這個請求委託給它的原型Animal 對象,如果Animal 對象也沒有這個屬性,那麼請求會順着原型鏈繼續被委託給Animal 對象的原型Object 對象,這樣一來便能得到繼承的效果,看起來就像Animal 是Dog 的“父類”,Object 是Animal 的“父類”。
原型編程範型至少包括以下基本規則:
- 所有的數據都是對象。
- 要得到一個對象,不是通過實例化類,而是找到一個對象作爲原型並克隆它。
- 對象會記住它的原型。
- 如果對象無法響應某個請求,它會把這個請求委託給它自己的原型。
1、爲什麼所有數據都是對象?
Javascript的數據分爲基本類型和對象類型,基本類型包括:undefined、number、boolean、string、function、object。按照JavaScript設計者的本意,除了undefined 之外,一切都應是對象。爲了實現這一目標,number、boolean、string 這幾種基本類型數據也可以通過“包裝類”的方式變成對象類型數據來處理。
事實上,JavaScript 中的根對象是Object.prototype 對象。Object.prototype 對象是一個 空的對象。我們在JavaScript 遇到的每個對象,實際上都是從Object.prototype 對象克隆而來的,Object.prototype 對象就是它們的原型。比如下面的obj1 對象和obj2 對象:
var obj1 = new Object();
var obj2 = {};
可以利用ECMAScript 5 提供的Object.getPrototypeOf 來查看這兩個對象的原型:
console.log( Object.getPrototypeOf( obj1 ) === Object.prototype ); // 輸出:true
console.log( Object.getPrototypeOf( obj2 ) === Object.prototype ); // 輸出:true
2. 要得到一個對象,不是通過實例化類,而是找到一個對象作爲原型並克隆它,怎樣克隆?
在JavaScript 語言裏,我們並不需要關心克隆的細節,因爲這是 引擎內部負責實現的。我 們所需要做的只是顯式地調用
var obj1 = new Object()或者
var obj2 = {}。
此時,引擎內部會從 Object.prototype 上面克隆一個對象出來,我們最終得到的就是這個對象。
如何用new 運算符從構造器中得到一個對象,下面的代碼我們再熟悉不過了:
function Person( name ){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
var a = new Person( 'sven' )
console.log( a.name ); // 輸出:sven
console.log( a.getName() ); // 輸出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 輸出:true
在JavaScript 中 沒有類的概念,這句話我們已經重複過很多次了。但剛纔不是明明調用了newPerson()嗎?
在這裏Person 並不是類,而是 函數構造器,JavaScript 的函數既可以作爲普通函數被調用,也可以作爲構造器被調用。
當使用new 運算符來調用函數時,此時的函數就是一個構造器。 用new 運算符來創建對象的過程,實際上也只是先克隆Object.prototype 對象,再進行一些其他額外操作的過程。
3. 對象會記住它的原型
目前我們一直在討論“對象的原型”,就JavaScript 的真正實現來說,其實 並不能說對象有原型,而只能說對象的構造器有原型。 對於“對象把請求委託給它自己的原型”這句話,更好的說法是對象把請求委託給它的構造器的原型。
JavaScript 給對象提供了一個名爲 __proto__
的隱藏屬性,某個對象的__proto__
屬性默認會指向它的構造器的原型對象,即{Constructor}.prototype。在一些瀏覽器中,__proto__
被公開出來,我們可以在Chrome 或者Firefox 上用這段代碼來驗證:
var a = new Object();
console.log ( a.__proto__=== Object.prototype ); // 輸出:true
實際上,__proto__
就是對象跟“對象構造器的原型”聯繫起來的紐帶。
4. 如果對象無法響應某個請求,它會把這個請求委託給它的構造器的原型
這條規則即是原型繼承的精髓所在。
實際上,雖然JavaScript 的對象最初都是由Object.prototype 對象克隆而來的,但對象構造器的原型並不僅限於Object.prototype 上,而是可以動態指向其他對象。這樣一來,當對象a 需要借用對象b 的能力時,可以有選擇性地把對象a 的構造器的原型指向對象b,從而達到繼承的效果。下面的代碼是我們最常用的原型繼承方式:
var obj = { name: 'sven' };
var A = function(){};
A.prototype = obj;
var a = new A();
console.log( a.name ); // 輸出:sven
我們來看看執行這段代碼的時候,引擎做了哪些事情。
- 首先,嘗試遍歷對象a 中的所有屬性,但沒有找到name 這個屬性。
- 查找name 屬性的這個請求被委託給對象a 的構造器的原型,它被
a.__proto__
記錄着並且指A.prototype
,而A.prototype
被設置爲對象obj。 - 在對象obj 中找到了name 屬性,並返回它的值。
當我們期望得到一個“類”繼承自另外一個“類”的效果時,往往會用下面的代碼來模擬實現:
var A = function(){};
A.prototype = { name: 'sven' };
var B = function(){};
B.prototype = new A();
var b = new B();
console.log( b.name ); // 輸出:sven
再看這段代碼執行的時候,引擎做了什麼事情。
- 首先,嘗試遍歷對象b 中的所有屬性,但沒有找到name 這個屬性。
- 查找name 屬性的請求被委託給對象b 的構造器的原型,它被
b.__proto__
記錄着並且指向B.prototype,而B.prototype
被設置爲一個通過new A()創建出來的對象。 - 在該對象中依然沒有找到name 屬性,於是請求被繼續委託給這個對象構造器的原型
A.prototype
。 - 在
A.prototype
中找到了name 屬性,並返回它的值。
和把B.prototype 直接指向一個字面量對象相比,通過B.prototype = new A()形成的原型鏈比之前多了一層。但二者之間沒有本質上的區別,都是 將對象構造器的原型指向另外一個對象,繼承總是發生在對象和對象之間。
最後還要留意一點,原型鏈並不是無限長的。現在我們嘗試訪問對象a 的address 屬性。而對象b 和它構造器的原型上都沒有address 屬性,那麼這個請求會被最終傳遞到哪裏呢?
實際上,當請求達到A.prototype,並且在A.prototype 中也沒有找到address 屬性的時候,請求會被傳遞給A.prototype 的構造器原型Object.prototype,顯然Object.prototype 中也沒有address 屬性,但Object.prototype 的原型是null,說明這時候原型鏈的後面已經沒有別的節點了。所以該次請求就到此打住,a.address 返回undefined。
a.address // 輸出:undefined