天天看點

《JavaScript面向對象程式設計指南(第2版)》——1.6 面向對象的程式設計

本節書摘來自異步社群《javascript面向對象程式設計指南(第2版)》一書中的第1章,第1.6節,作者:【加拿大】stoyan stefanov著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

在深入學習javascript之前,我們首先要了解一下“面向對象”的具體含義,以及這種程式設計風格的主要特征。下面我們列出了一系列在面向對象程式設計(oop)中最常用到的概念:

對象、方法、屬性;

類;

封裝;

聚合;

重用與繼承;

多态。

現在,我們就來詳細了解每個概念。當然,如果您在面向對象程式設計方面是一個新手,或者不能确定自己是否真的了解了這些概念,那也不必太過擔心。以後我們還會通過一些代碼來為您具體分析它們。盡管這些概念說起來好像很複雜、很進階,但一旦我們進入真正的實踐,事情往往就會簡單得多。

1.6.1 對象

既然這種程式設計風格叫做面向對象,那麼其重點就應該在對象上。而所謂對象,實質上就是指“事物”(包括人和物)在程式設計語言中的表現形式。這裡的“事物”可以是任何東西(如某個客觀存在的對象,或者某些較為抽象的概念)。例如,對于貓這種常見對象來說,我們可以看到它們具有某些明确的特征(如顔色、名字、體型等),能執行某些動作(如喵喵叫、睡覺、躲起來、逃跑等)。在oop語義中,這些對象特征都叫做屬性,而那些動作則被稱為方法。

此外,我們還有一個口語方面的類比1。

對象往往是用名詞來表示的(如book、person)。

方法一般都是些動詞(如read、run)。

屬性值則往往是一些形容詞。

我們可以試一下。例如,在“the black cat sleeps on my head”這個句子中,“the cat”(名詞)就是一個對象,“black”(形容詞)則是一個顔色屬性值,而“sleep”(動詞)則代表一個動作,也就是oop語義中的方法。甚至,為了進一步證明這種類比的合理性,我們也可以将句子中的“on my head”看做動作“sleep”的一個限定條件,是以,它也可以被當做傳遞給sleep方法的一個參數。

1.6.2 類

在現實生活中,相似對象之間往往都有一些共同的組成特征。例如蜂鳥和老鷹都具有鳥類的特征,是以它們可以被統稱為鳥類。在oop中,類實際上就是對象的設計藍圖或制作配方。“對象”這個詞,我們有時候也叫做“執行個體”,是以我們可以說老鷹是鳥類的一個執行個體2。我們可以基于同一個類建立出許多不同的對象。因為類更多的是一種模闆,而對象則是在這些模闆的基礎上被建立出來的實體。

但我們要明白,javascript與c++或java這種傳統的面向對象語言不同,它實際上壓根兒沒有類。該語言的一切都是基于對象的,其依靠的是一套原型(prototype)系統。而原型本身實際上也是一種對象,我們後面也會再來詳細讨論這個問題。在傳統的面向對象語言中,我們一般會這樣描述自己的做法:“我基于person類建立了一個叫做bob的新對象。”而在這種基于原型的面向對象語言中,我們則要這樣描述:“我将現有的person對象擴充成了一個叫做bob的新對象。”

1.6.3 封裝

封裝是另一個與oop相關的概念,其主要用于闡述對象中所包含的内容。封裝概念通常由兩部分組成。

相關的資料(用于存儲屬性)。

基于這些資料所能做的事(所能調用的方法)。

除此之外,這個術語中還有另一層資訊隐藏的概念,這完全是另一方面的問題。是以,我們在了解這個概念時,必須要留意它在oop中的具體語境。

以一個mp3播放器為例。如果我們假設它是一個對象,那麼作為該對象的使用者,我們無疑需要一些類似于像按鈕、顯示屏這樣的工作接口。這些接口會幫助我們使用該對象(如播放歌曲之類)。至于它們内部是如何工作的,我們并不清楚,而且大多數情況下也不會在乎這些。換句話說,這些接口的實作對我們來說是隐藏的。同樣的,在oop中也是如此。當我們在代碼中調用一個對象的方法時,無論該對象是來自我們自己的實作還是某個第三方庫,我們都不需要知道該方法是如何工作的。在編譯型語言中,我們甚至都無法檢視這些對象的工作代碼。由于javascript是一種解釋型語言,源代碼是可以檢視的。但至少在封裝概念上它們是一緻的,即我們隻需要知道所操作對象的接口,而不必去關心它的具體實作。

關于資訊隐藏,還有另一方面内容,即方法與屬性的可見性。在某些語言中,我們能通過public、private、protected這些關鍵字來限定方法和屬性的可見性。這種限定分類定義了對象使用者所能通路的層次。例如,private方法隻有其所在對象内部的代碼才有權通路,而public方法則是任何人都能通路的。在javascript中,盡管所有的方法和屬性都是public的,但是我們将會看到,該語言還是提供了一些隐藏資料的方法,以保護程式的隐密性。

1.6.4 聚合

所謂聚合,有時候也叫做組合,實際上是指我們将幾個現有對象合并成一個新對象的過程。總之,這個概念所強調的就是這種将多個對象合而為一的能力。通過聚合這種強有力的方法,我們可以将一個問題分解成多個更小的問題。這樣一來,問題就會顯得更易于管理(便于我們各個擊破)。當一個問題域的複雜程度令我們難以接受時,我們就可以考慮将它分解成若幹子問題區,并且必要的話,這些問題區還可以再繼續分解成更小的分區。這樣做有利于我們從幾個不同的抽象層次來考慮這個問題。

例如,個人電腦是一個非常複雜的對象,我們不可能知道它啟動時所發生的全部事情。但如果我們将這個問題的抽象級别降低到一定的程度,隻關注它幾個元件對象的初始化工作,例如顯示器對象、滑鼠對象、鍵盤對象等,我們就很容易深入了解這些子對象情況,然後再将這些部分的結果合并起來,之前那個複雜問題就迎刃而解了。

我們還可以找到其他類似情況,例如book是由一個或多個author對象、publisher對象、若幹chapter對象以及一組table對象等組合(聚合)而成的對象。

1.6.5 繼承

通過繼承這種方式,我們可以非常優雅地實作對現有代碼的重用。例如,我們有一個叫做person的一般性對象,其中包含一些姓名、出生日期之類的屬性,以及一些功能性函數,如步行、談話、睡覺、吃飯等。然後,當我們發現自己需要一個programmer對象時,當然,這時候你可以再将person對象中所有的方法與屬性重新實作一遍,但除此之外還有一種更聰明的做法,即我們可以讓programmer繼承自person,這樣就省去了我們不少工作。因為programmer對象隻需要實作屬于它自己的那部分特殊功能(例如“編寫代碼”),而其餘部分隻需重用person的實作即可。

在傳統的oop環境中,繼承通常指的是類與類之間的關系,但由于javascript中不存在類,是以它的繼承隻能發生在對象之間。

當一個對象繼承自另一個對象時,通常會往其中加入新的方法,以擴充被繼承的老對象。我們通常将這一過程稱之為“b繼承自a”或者“b擴充自a”。另外對于新對象來說,它也可以根據自己的需要,從繼承的那組方法中選擇幾個來重新定義。這樣做并不會改變對象的接口,因為其方法名是相同的,隻不過當我們調用新對象時,該方法的行為與之前不同了。我們将這種重定義繼承方法的過程叫做覆寫。

1.6.6 多态

在之前的例子中,我們的programmer對象繼承了上一級對象person的所有方法。這意味着這兩個對象都實作了“talk”等方法。現在,我們的代碼中有一個叫做bob的變量,即便是在我們不知道它是一個person對象還是一個programmer對象情況下,也依然可以直接調用該對象的“talk”方法,而不必擔心這會影響代碼的正常工作。類似這種不同對象通過相同的方法調用來實作各自行為的能力,我們就稱之為多态。