天天看點

JavaScript繼承詳解

幾乎每個開發人員都有面向對象語言(比如c++、c#、java)的開發經驗。 在傳統面向對象的語言中,有兩個非常重要的概念 - 類和執行個體。 類定義了一類事物公共的行為和方法;而執行個體則是類的一個具體實作。 我們還知道,面向對象程式設計有三個重要的概念 - 封裝、繼承和多态。

但是在javascript的世界中,所有的這一切特性似乎都不存在。 因為javascript本身不是面向對象的語言,而是基于對象的語言。 這裡面就有一些有趣的特性,比如javascript中所有事物都是對象, 包括字元串、數組、日期、數字,甚至是函數,比如下面這個例子:

1

2

3

4

5

6

7

8

9

10

<code>// 定義一個函數 - add</code>

<code>function</code> <code>add(a, b) {</code>

<code>    </code><code>add.invoketimes++;</code>

<code>    </code><code>return</code> <code>a + b;</code>

<code>}</code>

<code>// 因為函數本身也是對象,這裡為函數add定義一個屬性,用來記錄此函數被調用的次數</code>

<code>add.invoketimes = 0;</code>

<code>add(1 + 1);</code>

<code>add(2 + 3);</code>

<code>console.log(add.invoketimes);</code><code>// 2</code>

在面向對象的語言中,我們使用類來建立一個自定義對象。然而javascript中所有事物都是對象,那麼用什麼辦法來建立自定義對象呢?

這就需要引入另外一個概念 - 原型(prototype),我們可以簡單的把prototype看做是一個模版,新建立的自定義對象都是這個模版(prototype)的一個拷貝 (實際上不是拷貝而是連結,隻不過這種連結是不可見,給人們的感覺好像是拷貝)。

讓我們看一下通過prototype建立自定義對象的一個例子:

11

12

13

14

<code>// 構造函數</code>

<code>   </code><code>function</code> <code>person(name, sex) {</code>

<code>       </code><code>this</code><code>.name = name;</code>

<code>       </code><code>this</code><code>.sex = sex;</code>

<code>   </code><code>}</code>

<code>   </code><code>// 定義person的原型,原型中的屬性可以被自定義對象引用</code>

<code>   </code><code>person.prototype = {</code>

<code>       </code><code>getname:</code><code>function</code><code>() {</code>

<code>           </code><code>return</code> <code>this</code><code>.name;</code>

<code>       </code><code>},</code>

<code>       </code><code>getsex:</code><code>function</code><code>() {</code>

<code>           </code><code>return</code> <code>this</code><code>.sex;</code>

<code>       </code><code>}</code>

這裡我們把函數person稱為構造函數,也就是建立自定義對象的函數。可以看出,javascript通過構造函數和原型的方式模拟實作了類的功能。 

建立自定義對象(執行個體化類)的代碼:

<code>var</code> <code>zhang =</code><code>new</code> <code>person(</code><code>"zhangsan"</code><code>,</code><code>"man"</code><code>);</code>

<code>console.log(zhang.getname());</code><code>// "zhangsan"</code>

<code>var</code> <code>chun =</code><code>new</code> <code>person(</code><code>"chunhua"</code><code>,</code><code>"woman"</code><code>);</code>

<code>console.log(chun.getname());</code><code>// "chunhua"</code>

當代碼var zhang = new person("zhangsan", "man")執行時,其實内部做了如下幾件事情:

建立一個空白對象(new object())。

拷貝person.prototype中的屬性(鍵值對)到這個空對象中(我們前面提到,内部實作時不是拷貝而是一個隐藏的連結)。

将這個對象通過this關鍵字傳遞到構造函數中并執行構造函數。

将這個對象指派給變量zhang。

為了證明prototype模版并不是被拷貝到執行個體化的對象中,而是一種連結的方式,請看如下代碼:

<code>function</code> <code>person(name, sex) {</code>

<code>    </code><code>this</code><code>.name = name;</code>

<code>    </code><code>this</code><code>.sex = sex;</code>

<code>person.prototype.age = 20;</code>

<code>console.log(zhang.age);</code><code>// 20</code>

<code>// 覆寫prototype中的age屬性</code>

<code>zhang.age = 19;</code>

<code>console.log(zhang.age);</code><code>// 19</code>

<code>delete</code> <code>zhang.age;</code>

<code>// 在删除執行個體屬性age後,此屬性值又從prototype中擷取</code>

這種在javascript内部實作的隐藏的prototype連結,是javascript賴以生存的溫潤土壤, 也是模拟實作繼承的基礎。

如何在javascript中實作簡單的繼承? 

下面的例子将建立一個雇員類employee,它從person繼承了原型prototype中的所有屬性。

<code>function</code> <code>employee(name, sex, employeeid) {</code>

<code>    </code><code>this</code><code>.employeeid = employeeid;</code>

<code>// 将employee的原型指向person的一個執行個體</code>

<code>// 因為person的執行個體可以調用person原型中的方法, 是以employee的執行個體也可以調用person原型中的所有屬性。</code>

<code>employee.prototype =</code><code>new</code> <code>person();</code>

<code>employee.prototype.getemployeeid =</code><code>function</code><code>() {</code>

<code>    </code><code>return</code> <code>this</code><code>.employeeid;</code>

<code>};</code>

<code>var</code> <code>zhang =</code><code>new</code> <code>employee(</code><code>"zhangsan"</code><code>,</code><code>"man"</code><code>,</code><code>"1234"</code><code>);</code>

<code>console.log(zhang.getname());</code><code>// "zhangsan</code>

上面關于繼承的實作很粗糙,并且存在很多問題:

在建立employee構造函數和原型(以後簡稱類)時,就對person進行了執行個體化,這是不合适的。

employee的構造函數沒法調用父類person的構造函數,導緻在employee構造函數中對name和sex屬性的重複指派。

employee中的函數會覆寫person中的同名函數,沒有重載的機制(和上一條是一個類型的問題)。

建立javascript類的文法過于零散,不如c#/java中的文法優雅。

實作中有constructor屬性的指向錯誤,這個會在第二篇文章中讨論。

我們會在第三章完善這個例子。

正因為javascript本身沒有完整的類和繼承的實作,并且我們也看到通過手工實作的方式存在很多問題, 是以對于這個富有挑戰性的任務網上已經有很多實作了:

douglas crockford - prototypal inheritance in javascript

douglas crockford - classical inheritance in javascript

john resig - simple javascript inheritance

dean edwards - a base class for javascript inheritance

prototype

mootools

extjs

這個系列的文章将會逐一深入分析這些實作,最終達到對javascript中如何實作類和繼承有一個深入的了解。

下一章我們将會介紹在類實作中的相關知識,比如this、constructor、prototype等。

本系列文章清單:

javascript繼承詳解

javascript繼承詳解(二)

javascript繼承詳解(三)

javascript繼承詳解(四)

javascript繼承詳解(五)

javascript繼承詳解(六)

繼續閱讀