天天看點

JavaScript設計模式與開發實踐 | 01 - 面向對象的JavaScript

動态類型語言

程式設計語言按資料類型大體可以分為兩類:靜态類型語言與動态類型語言。

靜态類型語言在編譯時已确定變量類型,

動态類型語言的變量類型要到程式運作時,待變量被指派後,才具有某種類型。

而JavaScript是一門典型的動态類型語言。

動态類型語言對變量類型的寬容使得程式設計變得很靈活。由于不用進行類型檢測,我們可以調用任何對象的任意方法,而無需去考慮它原本是否被設計為擁有該方法。而這是建立在鴨子類型的概念上。

鴨子類型

鴨子類型通俗的說法是:

如果它走起路來像鴨子,叫起來也是鴨子,那麼它就是鴨子。

鴨子類型指導我們隻關注對象的行為,而不關注對象本身。

在動态類型語言的面向對象設計中,利用鴨子類型的思想,我們不必借助超類型的幫助,就能輕松地在動态類型語言中實作一個原則:“面向接口程式設計,而不是面向實作程式設計”。例如:

  • 一個對象若有push和pop方法,并且這些方法提供了正确的實作,它就可以被當作棧來使用;
  • 一個對象若有length屬性,且可以依照下标來存取屬性,這個對象就可以被當作數組來使用。

多态

多态的含義

同一操作作用于不同的對象上面,可以産生不同的解釋和不同的執行結果。

對象的多态性

我們說的多态性,其實就是對象的多态性,那麼,對象的多态性是怎樣的?如何讓對象表現出多态性?

對象多态性的一個簡單的例子:

// 讓動物發聲
var makeSound = function(animal){
    animal.sound();
}
// 鴨子的叫聲
var Duck = function(){};
Duck.prototype.sound = function(){
    console.log('嘎嘎嘎');
};
// 小雞的叫聲
var Chicken = function(){};
Chicken.prototype.sound = function(){
    console.log('咯咯咯');
}

// 讓鴨子發聲
makeSound(new Duck());
// 讓小雞發聲
makeSound(new Chicken()); 

// 如果像讓小狗發聲,隻需要簡單地追加類似的代碼
var Dog = function(){};
Dog.prototype.sound = function(){
    console.log('汪汪汪');
}
makeSound(new Dog());
           

類型檢查

靜态類型語言(例如Java)在編譯時會進行類型比對檢查,這種檢查在帶來安全性的同時,讓代碼顯得僵硬。是以,靜态類型語言通常被設計為可以向上轉型:

當給一個類變量指派時,這個變量的類型既可以使用這個類本身,也可以使用這個類的超類。

就像我們在描述“一隻麻雀在飛”,“一隻喜鵲在飛”時,如果想忽略他們的具體類型,可以說成“一隻鳥在飛”,這時“鳥”就是“麻雀”和“喜鵲”的超類。

而JavaScript是一門不必進行類型檢查的動态類型語言。

多态的作用

多态是面向對象程式設計語言中最重要的技術。

多态最根本的作用就是通過把過程化的條件分支語句轉化為對象的多态性,進而消除這些條件分支語句。有一個例子可以很好地诠釋:

在電影的拍攝現場,當導演喊出“anciton”時,主角開始背台詞,照明師負責打燈光,後面的群衆演員假裝中槍倒地,道具師往鏡頭裡撒上雪花。在得到同一個消息時,每個對象都知道自己應該做什麼。如果不利用對象的多态性,而是用面向過程的方式來編寫這一段代碼,那麼相當于在電影開始拍攝後,導演每次都要走到每個人的面前,确認他們的職業分工(類型),然後告訴他們要做什麼。如果映射到程式中,那麼程式中将充斥着條件分支語句。

将行為分布在各個對象中,并讓這些對象各自負責自己的行為,這正是面向對象設計的優點。

多态與設計模式

從面向對象設計的角度出發,通過對封裝、繼承、多态、組合等技術的反複使用,提煉出一些可重複使用的面向對象設計技巧,我們将其歸納為設計模式。而多态在其中是重中之重,絕大多部分設計模式的實作都離不開多态性的思想。例如:

  • 指令模式
  • 組合模式
  • 政策模式
  • ...

Javascript将函數作為一等對象,是以函數本身也是對象,函數用來封裝行為并且能夠四處傳遞。當我們對一些函數發出“調用”的消息時,這些函數會傳回不同的執行結果,這是多态性的一種展現。

封裝

封裝的目的是将資訊隐藏。封裝包括:

  • 封裝資料
  • 封裝實作
  • 封裝類型
  • 封裝變化

封裝資料

在許多語言的對象系統中,封裝資料是由文法解析來實作的,這些語言可能提供了private、public、protected等關鍵字來提供不同的通路權限。但JavaScript并沒有提供對這些關鍵字的支援,隻能依賴變量的作用域來實作封裝特性,而且隻能模拟出public和private這兩種封裝性。

一般我們通過函數來建立作用域:

var myObject = (function(){
    var __name = 'sven';    //私有(private)變量
    return {
        getName:function(){        //公開(public)方法
            return __name;
        }
    }
})();

console.log(myObject.getName());    //輸出:sven
console.log(myObject.__name);        //輸出:undefined
           

封裝實作

從封裝實作細節來講,封裝使得對象内部的變化對其他對象而言是透明的(即不可見)。對象對它自己的行為負責。其他對象或者使用者都不關心它的内部實作。對象使得對象之間的耦合變得松散,對象之間隻通過暴露的API接口來通信。

封裝實作細節的例子非常多,例如疊代器。疊代器的作用是在不暴露一個聚合對象的内部表示的前提下,提供一種方式來順序通路這個聚合對象。如一個each函數,它的作用就是周遊一個聚合對象,使用這個each函數的人不用關心它的内部代碼是怎麼實作的,隻要它提供的功能正确便可以了。

封裝類型

封裝類型是靜态類型語言的一種重要封裝方式。封裝類型是通過抽象類和接口來進行的。

在JavaScript中,并沒有對抽象類和接口的支援。JavaScript本身也是一門類型模糊的語言。在封裝類型方面,JavaScript沒有能力,也沒有必要做得更多。

封裝變化

從設計模式的角度出發,封裝在更重要的層面展現為封裝變化。

通過封裝變化的方式,把系統中穩定不變的部分和容易變化的部分隔離開來,在系統的演變過程中,我們隻需要替換掉那些容易變化的部分,如果這些部分是已經封裝好的,替換起來也想對容易。這可以最大程度地保證程式的穩定性和可擴充性。

原型模式

原型模式

原型模式是用于建立對象的一種模式。

原型模式不用關心對象的具體類型,隻需找到一個對象,然後通過克隆來創造一個一模一樣的對象。

原型模式的實作關鍵是語言本身是否提供了clone方法,ECMAScript5提供了Object.create方法,可以用來克隆對象。

原型模式的真正目的不在于需要得到一模一樣的對象,而是提供了一種便捷的方式去建立某個類型的對象,克隆隻是建立這個對象的過程和手段。

在JavaScript這種類型模糊的語言中,建立對象非常容易,也不存在類型耦合的問題。從設計模式的角度來看,原型模式的意義并不算大。但JavaScript本身是一門基于原型的面向對象語言,它的對象系統就是使用原型模式來搭建的,在這裡稱為原型程式設計範型也許更合适。

原型程式設計範型

原型程式設計中有一個重要特性,即當對象無法響應某個請求時,會把該請求委托給它自己的原型。

而原型程式設計範型至少包括以下基本原則:

  • 所有的資料都是對象
  • 要得到一個對象,不是通過執行個體化類,而是找到一個對象作為原型并克隆它
  • 對象會記住它的原型
  • 如果對象無法響應某個請求,它會把這個請求委托給它自己的原型

JavaScript中的原型繼承

JavaScript在原型程式設計範型的規則的基礎上來建構它的對象系統。

所有的資料都是對象

JavaScript在設計的時候,模仿Java引入了兩套類型機制:基本類型和對象類型。

按照JavaScript設計者的本意,除了undefined之外,一切都應是對象。為了實作這一目标,number、boolean等幾種基本類型資料可以通過“包裝類”的方式變成對象類型資料。

JavaScript絕大部分資料都是對象。事實上,JavaScript中的根對象是Object.prototype對象。Object.prototype對象是一個空對象。JavaScript的每個對象,都是從Object.prototype對象克隆而來。

要得到一個對象,不是通過執行個體化類,而是找到一個對象作為原型并克隆它

JavaScript通過顯式地調用

var obj1 = new Object()

, 或者

var obj2 = {}

。此時,引擎内部會從Object.prototype上面克隆一個對象出來。

這裡用了new運算符從構造器中得到了一個對象。在JavaScript裡,函數既可以作為普通的函數被調用,也可以作為構造器被調用。用new運算符來建立對象的過程,實際上也隻是先克隆Object.prototype對象,再進行一些其他額外操作的過程。

對象會記住它的原型

就JavaScript的真正實作來說,其實并不能說對象有原型,而隻能說對象的構造器有原型。“對象把請求委托給它自己的原型”就是對象把請求委托給它的構造器的原型。

JavaScript給對象提供了一個名為__proto__的隐藏屬性,某個對象的__proto__屬性預設會指向它的構造器的原型對象,即{Constructor}.prototype。在一些浏覽器中,__proto__被公開出來。

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

這條規則是原型繼承的精髓所在。當一個對象無法響應某個請求時,它會順着原型鍊把請求傳遞下去,直到遇到一個可以處理請求的對象為止。

雖然JavaScript的對象最初都是由Object.prototype對象克隆而來,但對象構造器的原型并不僅限于Object.prototype上,而是可以動态地指向其他對象。例如,當對象A需要對象B的能力時,可以有選擇地把對象A的構造器的原型指向對象B,進而達到繼承的效果。

PS:本節内容為《JavaScript設計模式與開發實踐》第一章 筆記。