1.4-原型

參考書籍: Javascript設計模式與開發實踐(曾探)

原型模式不單是一種設計模式,也被稱爲一種編程泛型。

原型模式 找到一個對象,然後通過 克隆 來創建一個一模一樣的對象。使用原型模式,我們只需要調用負責克隆的方法,便能完成同樣的功能。

ECMAScript 5 提供了Object.create方法,可以用來克隆對象。

但原型模式的真正目的並非在於需要得到一個一模一樣的對象,而是 提供了一種便捷的方式去創建某個類型的對象,克隆只是創建這個對象的過程和手段

Object 是Animal 的原型,而Animal 是Dog 的原型,它們之間形成了一條原型鏈。這個原型鏈是很有用處的,當我們嘗試調用Dog 對象的某個方法時,而它本身卻沒有這個方法,那麼Dog 對象會把這個請求委託給它的原型Animal 對象,如果Animal 對象也沒有這個屬性,那麼請求會順着原型鏈繼續被委託給Animal 對象的原型Object 對象,這樣一來便能得到繼承的效果,看起來就像Animal 是Dog 的“父類”,Object 是Animal 的“父類”。

原型編程範型至少包括以下基本規則:

  1. 所有的數據都是對象。
  2. 要得到一個對象,不是通過實例化類,而是找到一個對象作爲原型並克隆它。
  3. 對象會記住它的原型。
  4. 如果對象無法響應某個請求,它會把這個請求委託給它自己的原型。

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

我們來看看執行這段代碼的時候,引擎做了哪些事情。

  1. 首先,嘗試遍歷對象a 中的所有屬性,但沒有找到name 這個屬性。
  2. 查找name 屬性的這個請求被委託給對象a 的構造器的原型,它被a.__proto__ 記錄着並且指A.prototype,而A.prototype 被設置爲對象obj。
  3. 在對象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

再看這段代碼執行的時候,引擎做了什麼事情。

  1. 首先,嘗試遍歷對象b 中的所有屬性,但沒有找到name 這個屬性。
  2. 查找name 屬性的請求被委託給對象b 的構造器的原型,它被b.__proto__ 記錄着並且指向B.prototype,而B.prototype 被設置爲一個通過new A()創建出來的對象。
  3. 在該對象中依然沒有找到name 屬性,於是請求被繼續委託給這個對象構造器的原型A.prototype
  4. 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章