GOF設計模式(三)——原型模式和基於原型集成的JavaScript對象系統

前言

在傳統的面向對象編程語言中(如Java,C#), 類和對象就像柏拉圖所說的“模子”和薑餅人,所有的薑餅人(對象)總是從模子(類)中創建而來,類生成實例就像廚師用模子做出薑餅人。

而在原型編程的思想中,類並不是必須的,對象未必從類中創建,而是通過克隆另一個對象得到。

爲了更清晰地瞭解原型模式,將其與面向對象編程的思想區分開,我們先來了解一種輕巧又基於原型的語言—— Io語言。

IO語言

Io語言在2002年由 Steve Dekorte發明。可以從 http://iolanguage.com 下載到Io語言的解釋器。

作爲一門基於原型的語言,Io中沒有類的概念,每一個對象都是基於另外一個對象的克隆。而所有對象的祖先,即根對象,在Io語言中叫 Object.

現在我們有了根對象Object, 想要創建一個對象叫做Animal, 那麼Animal要從Object中複製而來,而Object就是Animal的原型。

 Aniaml := Object clone //克隆動物對象

假設在Io的世界中,所有的Animal都會叫,我們給Animal添加makeSound方法:

Animal makeSound := method( "animal makeSound " print ); 

接着,我們再創建一個 Dog 對象:

Dog := Animal clone

Dog 能夠搖尾巴

Dog wagTail := method( "Dog is wagging his tail" print );

以此類推,我們可以創建出更多的對象。在以上舉出的例子中存在這樣的原型鏈:

Dog
Animal
Object

原型編程範型的一些規則

Dog對象沒有makeSound方法,於是把請求委託給了他的原型Animal, 而Animal對象是有makeSound方法的,所以執行Dog makeSound 可以順利輸出 animal makeSound.
這個機制很簡單,但功能卻很強大,Java Script和Io一樣,基於原型鏈的委託機制就是原型繼承的本質
現在我們明白了原型編程中的一個重要特性,即當對象無法響應某個請求時,會把該請求委託給自己的原型。
總結一下原型編程範型的基本規則:

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

JavaScript中的原型繼承

依照原型範型的四條規範,我們來看JavaScript是如何一一實現的。

1. 所有的數據都是對象

JavaScript在設計的時候,模仿java設計了兩種數據類型: 基本類型對象類型

基本類型包括 undefined, number, boolean, string, function, object 。按照JS設計者的本意,除了undefined之外,一切都應是對象。爲了實現這一目標, number, boolean, string 這幾種基本類型數據也可以通過 包裝類 的方式變成對象類型數據來處理。

我們不能說JS中所有數據都是對象,但可以說絕大部分數據都是對象。

Io語言中的根對象是Object , 那麼JS中的根對象是什麼呢?

Object.prototype 對象就是這個根對象。
Object.prototype 對象是一個空對象。
JS中的每一個對象都是從 Object.prototype 對象克隆而來。Object.prototype 對象就是它們的原型。例如

var obj1 = new Object();
var obj2 = {};

Object.getPrototypeOf 來查看這兩個對象的原型:

console.log(Object.getPrototypeOf(obj1 === Object.prototype));  //true
console.log(Object.getPrototypeOf(obj2 === Object.prototype));  //true

2. 要得到一個對象,不是通過實例化類,而是找到一個對象將其作爲原型並克隆它

在Io語言中克隆有一個明顯的標誌,就是關鍵字 clone, 而在JS中,這個過程由引擎內部實現。我們只需要顯示地調用var obj = new object()obj2 = {},引擎內部會從 Object.prototype 上面克隆一個對象出來。

new運算符是如何得到一個對象的呢?例如:

function Person(name){
  this.name = name;
}

Person.prototype.getName = function(){
  return this.name;
}

var a = new Person( 'Mary' ) ;

console.log( a.name ); //Mary
console.log( a.getName() ); //Mary
console.log( Object.getPrototypeOf( a ) === Person.prototype ); //true

在這裏Person並不是類,而是函數構造器, JS的函數既可以作爲普通函數背調用,也可以作爲構造器被調用。當使用 new 運算符 來調用時就是一個構造器。
用 new 操作符來創建對象,實際上也只是先克隆Person.prototype對象,再進行一些其他額外操作的過程。

我們可以通過下面這段代碼來理解new運算的過程:

function Person(name){
  this.name = name;
}

Person.prototype.getName = function(){
  return this.name;
}

objectFactory() {
  let obj = new Object(), //從Object.prototype上克隆一個空的對象
      Constructor = [].shift.call(arguments); //取得外部傳入的構造器, 此例是Person
  obj._proto__ = Constructor.prototype; //指向正確的原型
  let ret = Constructor.apply(obj, arguments);
  return typeof ret === 'object' ? ret : obj;
}

3. 對象會記住自己的原型

就JS的真正實現來說, 其實並不能說對象有原型,而只能說對象的構造器有原型。所以,對於 “ 對象把請求委託給它自己的原型” 這句話,更好的說法是對象把請求委託給它的構造器原型
JS給對象提供了一個名爲 “proto” 的隱藏屬性指向它的構造器的原型對象,即 {Constructor}.prototype。

4. 如果對象無法響應某個請求,它會把這個請求委託給自己的原型

在之前Io語言的舉例中,因爲每個Io對象都可以作爲原型被克隆,所以存在一條天然的原型鏈:

Dog
Animal
Object

而在JS中,每個對象都是從 Object.prototype 對象克隆而來。如果這樣的話,我們只能得到非常單一的繼承關係,即每個對象都繼承自 Object.prototype 對象,這樣的對象系統十分受限。

實際上,雖然JS的對象最初都是 Object.prototype 對象克隆而來的,但對象構造器的原型並不僅限於Object.prototype上,而是可以動態指向其他對象。比如, 當對象a要借用對象b的方法時,可以有選擇性地將對象a的構造器的原型指向b,從而達到繼承的效果。

常見的原型繼承方式:

var obj = { name: 'Mary'};

var A = function(){};
A.prototype = obj;

var a = new A();
console.log(a.name);  // Mary

當我們期望得到一個“類”繼承自另外一個“類”的效果時(注意JS中並沒有真正的的“類”的概念),可以用下面的方式:

var Dog = function(){};
Dog.prototype = { call:'Wang!' };

var Samoyed = function(){};
Samoyed.prototype = new Dog(); 

var Maya = new Samoyed();
console.log(Maya.call);   // Wang!

分析下在執行這段代碼的時候,引擎做了什麼事情。

  • 嘗試遍歷對象 Maya 中的所有屬性,但沒有找到 name 這個屬性
  • 查找 name 屬性被委託給 Maya._proto_ ,即 Maya 構造器的prototype , 即 Samoyed.prototype, 而Samoyed.prototype 被設置爲一個通過 new Dog() 創建出來的對象。
  • 該對象中依然沒有 name 這個屬性,於是請求被委託給這個對象構造器的原型 Dog.prototype.
  • Dog.prototype 中找到name 屬性,並返回它的值。

和把Samoyed.prototype 直接指向一個字面量對象相比, 通過Samoyed.prototype = new Dog() 形成的原型鏈比之前多一層。但二者沒有實質區別,都是將對象構造器的原型指向另一個對象,繼承總是發生在對象和對象之間

原型繼承的未來

JS的 Object.create 是原型繼承的天然實現, 使用 Object.create 來完成原型繼承似乎更直觀(類似Io語言中的 clone)。美中不足是,當前JS引擎下,通過 Object.create 創建對象的效率不高,比通過構造函數要慢。

通過設置構造器 prototype 來實現原型繼承的時候,除了根對象 Object.prototype 本身之外, 任何對象都會有一個原型。而通過 Object.create(null) 可以創建出一個沒有原型的對象。

ECMAScript6 帶來了新的Class語法,使它看起來好像是一門基於類的語言,但其背後仍是通過原型機制來創建對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章