本節書摘來自異步社群《javascript設計模式》一書中的第2章,第2.2節,作者:張容銘著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視
2.2.1 建立一個類
“在javascript中建立一個類很容易,首先聲明一個函數儲存在一個變量裡。按程式設計習慣一般将這個代表類的變量名首字母大寫。然後在這個函數(類)的内部通過對this(函數内部自帶的一個變量,用于指向目前這個對象)變量添加屬性或者方法來實作對類添加屬性或者方法,例如:”
“也可以通過在類的原型(類也是一個對象,是以也有原型prototype)上添加屬性和方法,有兩種方式,一種是一一為原型對象屬性指派,另一種則是将一個對象指派給類的原型對象。但這兩種不要混用。例如:”
“這樣我們将所需要的方法和屬性都封裝在我們抽象的book類裡面了,當使用功能方法時,我們不能直接使用這個book類,需要用new關鍵字來執行個體化(建立)新的對象。使用執行個體化對象的屬性或者方法時,可以通過點文法通路,例如:”
小白看了看對類添加的屬性和方法部分,感覺不是很了解,于是問:“通過this添加的屬性和方法同在prototype中添加的屬性和方法有什麼差別呀?”
“通過this添加的屬性、方法是在目前對象上添加的,然而javascript是一種基于原型prototype的語言,是以每建立一個對象時(當然在javascript中函數也是一種對象),它都有一個原型prototype用于指向其繼承的屬性、方法。這樣通過prototype繼承的方法并不是對象自身的,是以在使用這些方法時,需要通過prototype一級一級查找來得到。這樣你會發現通過this定義的屬性或者方法是該對象自身擁有的,是以我們每次通過類建立一個新對象時,this指向的屬性和方法都會得到相應的建立,而通過prototype繼承的屬性或者方法是每個對象通過prototype通路到,是以我們每次通過類建立一個新對象時這些屬性和方法不會再次建立。(如圖2-1所示)。”
“哦,對了,解析圖中的constructor又是指的什麼呀。”
“constructor是一個屬性,當建立一個函數或者對象時都會為其建立一個原型對象prototype,在prototype對象中又會像函數中建立this一樣建立一個constructor屬性,那麼constructor屬性指向的就是擁有整個原型對象的函數或對象,例如在本例中book prototype中的constructor屬性指向的就是book類對象。”

2.2.2 這些都是我的——屬性與方法封裝
“原來是這樣,”小白似乎明白些,“面向對象思想在學校裡也學過,說的就是對一些屬性方法的隐藏與暴露,比如私有屬性、私有方法、共有屬性、共有方法、保護方法等等,那麼javascript中也有這些麼?”
“你能想到這些很好。說明你有一定面向對象的基礎了。不過你說的這些在javascript中沒有顯性的存在,但是我們可以通過一些靈活的技巧來實作它。”小銘繼續解釋說,“面向對象思想你可以想象成一個人,比如一位明星為了在社會中保持一個良好形象,她就會将一些隐私隐藏在心裡,然而對于這位明星,她的家人認識她,是以會了解一些關于她的事情。外界的人不認識她,即使外界人通過某種途徑認識她也僅僅了解一些她暴露出來的事情,不會了解她的隐私。如果想了解更多關于她的事情怎麼辦?對,還可以通過她的家人來了解,但是這位明星自己内心深處的隐私是永遠不會被别人知道的。”
“那麼在javascript中又是如何實作的呢?”小白問。
“由于javascript的函數級作用域,聲明在函數内部的變量以及方法在外界是通路不到的,通過此特性即可建立類的私有變量以及私有方法。然而在函數内部通過this建立的屬性和方法,在類建立對象時,每個對象自身都擁有一份并且可以在外部通路到。是以通過this建立的屬性可看作是對象共有屬性和對象共有方法,而通過this建立的方法,不但可以通路這些對象的共有屬性與共有方法,而且還能通路到類(建立時)或對象自身的私有屬性和私有方法,由于這些方法權利比較大,是以我們又将它看作特權方法。在對象建立時通過使用這些特權方法我們可以初始化執行個體對象的一些屬性,是以這些在建立對象時調用的特權方法還可以看作是類的構造器。如下面的例子。”
小白心中暗喜:“原來是這樣呀,通過javascript函數級作用域的特征來實作在函數内部建立外界就通路不到的私有化變量和私有化方法。通過new關鍵字執行個體化對象時,由于對類執行一次,是以類的内部this上定義的屬性和方法自然就可以複制到新建立的對象上,成為對象公有化的屬性與方法,而其中的一些方法能通路到類的私有屬性和方法,就像例子中家人對明星了解得比外界多,是以比外界權利大,因而得名特權方法。而我們在通過new關鍵字執行個體化對象時,執行了一遍類的函數,是以裡面通過調用特權方法自然就可以初始化對象的一些屬性了。可是在類的外部通過點文法定義的屬性和方法以及在外部通過prototype定義的屬性和方法又有什麼作用呢?”
“通過new關鍵字建立新對象時,由于類外面通過點文法添加的屬性和方法沒有執行到,是以新建立的對象中無法擷取他們,但是可以通過類來使用。是以在類外面通過點文法定義的屬性以及方法被稱為類的靜态共有屬性和類的靜态共有方法。而類通過prototype建立的屬性或者方法在類執行個體的對象中是可以通過this通路到的(如圖2.1新建立的對象的proto指向了類的原型所指向的對象),是以我們将prototype對象中的屬性和方法稱為共有屬性和共有方法,如:”
“通過new關鍵字建立的對象實質是對新對象this的不斷指派,并将prototype指向類的prototype所指向的對象,而類的構造函數外面通過點文法定義的屬性方法是不會添加到新建立的對象上去的。是以要想在新建立的對象中使用ischinese就得通過book類使用而不能通過this,如book.ischinese,而類的原型prototype上定義的屬性在新對象裡就可以直接使用,這是因為新對象的prototype和類的prototype指向的是同一個對象。”
于是小白半信半疑地寫下了測試代碼:
“真的是這樣,類的私有屬性num以及靜态共有屬性ischinese在新建立的b對象裡是通路不到的。而類的共有屬性isjsbook在b對象中卻可以通過點文法通路到。”
“但是類的靜态公有屬性ischinese可以通過類的自身通路。”
2.2.3 你們看不到我——閉包實作
“有時我們經常将類的靜态變量通過閉包來實作。”
“小白,你知道閉包麼?”
“不太了解。你能說說麼?”
“閉包是有權通路另外一個函數作用域中變量的函數,即在一個函數内部建立另外一個函數。我們将這個閉包作為建立對象的構造函數,這樣它既是閉包又是可執行個體對象的函數,即可通路到類函數作用域中的變量,如booknum這個變量,此時這個變量叫靜态私有變量,并且checkbook()可稱之為靜态私有方法。當然閉包内部也有其自身的私有變量以及私有方法如price,checkid()。但是,在閉包外部添加原型屬性和方法看上去像似脫離了閉包這個類,是以有時候在閉包内部實作一個完整的類然後将其傳回,看下面的例子。”
“哦,這樣看上去更像一個整體。”
2.2.4 找位檢察長——建立對象的安全模式
“對于你們初學者來說,在建立對象上由于不适應這種寫法,是以經常容易忘記使用new而犯錯誤。”
“可是對于我們來說,這種錯誤發生也是不可避免的,畢竟不像你們工作了這麼多年。但是你有什麼好辦法麼?”
“哈哈,那是當然,如果你們犯錯誤有人實時監測不就解決了麼,是以趕快找一位檢察長吧。比如javascript在建立對象時有一種安全模式就完全可以解決你們這類問題。”
“小白,你猜book這個變量是個什麼?”
“book類的一個執行個體吧。”為了驗證自己的想法,小白寫下測試代碼。
<code>console.log(book); // undefined</code>
“怎麼會是這樣?為什麼是一個undefined(未定義)?”小白不解。
“别着急,你來看看我的測試代碼。”
“怎麼樣發現問題了麼”,小銘問道。
“明明建立了一個book對象,并且添加了title、time、type3個屬性,怎麼會添加到window上面去了,而且book這個變量還是undefined。”小白又看了看執行個體中的代碼恍然大悟,“哦,原來是忘記了用new關鍵字來執行個體化了,可是為什麼會出現這個結果呢?”
“别着急,首先你要明白一點,new關鍵字的作用可以看作是對目前對象的this不停地指派,然而例子中沒有用new,是以就會直接執行這個函數,而這個函數在全局作用域中執行了,是以在全局作用域中this指向的目前對象自然就是全局變量,在你的頁面裡全局變量就是window了,是以添加的屬性自然就會被添加到window上面了,而我們這個book變量最終的作用是要得到book這個類(函數)的執行結果,由于函數中沒有return語句,這個book類自然不會告訴book變量的執行結果了,是以就是undefined(未定義)。”
“原來是這樣,看來建立時真是不小心呀,可是該如何避免呢?”小白感歎道。
“‘去找位檢察長’呀,哈哈,使用安全模式吧。”
“好了小白,測試一下吧。”
“真的是這樣呀,太好了,再也不用擔心建立對象忘記使用new關鍵字的問題了。”
“好了說了很多,你也休息一下,好好回顧一下,後面還有個更重要的面向對象等着你——繼承,這可是許多設計模式設計的靈魂。”