了解了 OOJS 的大多數(shù)細(xì)節(jié)之后,本文將介紹如何創(chuàng)建“子”對(duì)象類別(構(gòu)造器)并從“父”類別中繼承功能。此外,我們還會(huì)針對(duì)何時(shí)何處使用 OOJS 給出建議。
預(yù)備知識(shí): | 基本的計(jì)算機(jī)素養(yǎng),對(duì) HTML 和 CSS 有基本的理解,熟悉 JavaScript 基礎(chǔ)(參見 First steps 和 Building blocks)以及面向?qū)ο蟮腏avaScript (OOJS) 基礎(chǔ)(參見 Introduction to objects)。 |
---|---|
目標(biāo): | 理解在 JavaScript 中如何實(shí)現(xiàn)繼承。 |
到目前為止我們已經(jīng)了解了一些關(guān)于原型鏈的實(shí)現(xiàn)方式以及成員變量是如何通過它來實(shí)現(xiàn)繼承,但是之前涉及到的大部分都是瀏覽器內(nèi)置函數(shù)(比如 String
、Date
、Number
和 Array
),那么我們?nèi)绾蝿?chuàng)建一個(gè)繼承自另一對(duì)象的JavaScript對(duì)象呢?
正如前面課程所提到的,有些人認(rèn)為JavaScript并不是真正的面向?qū)ο笳Z言,在經(jīng)典的面向?qū)ο笳Z言中,你可能傾向于定義類對(duì)象,然后你可以簡(jiǎn)單地定義哪些類繼承哪些類(參考C++ inheritance里的一些簡(jiǎn)單的例子),JavaScript使用了另一套實(shí)現(xiàn)方式,繼承的對(duì)象函數(shù)并不是通過復(fù)制而來,而是通過原型鏈繼承(通常被稱為 原型式繼承 —— prototypal inheritance)。
讓我們通過具體的例子來解釋上述概念
首先,將oojs-class-inheritance-start.html文件復(fù)制到你本地(也可以點(diǎn)擊 running live 在線查看 ),其中你能看到一個(gè)只定義了一些屬性的Person構(gòu)造器,與之前通過模塊來實(shí)現(xiàn)所有功能的Person的構(gòu)造器類似。
function Person(first, last, age, gender, interests) { this.name = { first, last }; this.age = age; this.gender = gender; this.interests = interests;};
所有的方法都定義在構(gòu)造器的prototype上,比如:
Person.prototype.greeting = function() { alert('Hi! I\'m ' + this.name.first + '.');};
比如我們想要?jiǎng)?chuàng)建一個(gè)Teacher類,就像我們前面在面向?qū)ο蟾拍罱忉寱r(shí)用的那個(gè)一樣。這個(gè)類會(huì)繼承Person的所有成員,同時(shí)也包括:
subject
——這個(gè)屬性包含了教師教授的學(xué)科。greeting()
方法,這個(gè)方法打招呼聽起來比一般的greeting()
方法更正式一點(diǎn)——對(duì)于一個(gè)教授一些學(xué)生的老師來說。我們要做的第一件事是創(chuàng)建一個(gè)Teacher()
構(gòu)造器——將下面的代碼加入到現(xiàn)有代碼之下:
function Teacher(first, last, age, gender, interests, subject) { Person.call(this, first, last, age, gender, interests); this.subject = subject;}
這在很多方面看起來都和Person的構(gòu)造器很像,但是這里有一些我們從沒見過的奇怪玩意——call()
函數(shù)?;旧希@個(gè)函數(shù)允許你調(diào)用一個(gè)在這個(gè)文件里別處定義的函數(shù)。第一個(gè)參數(shù)指明了在你運(yùn)行這個(gè)函數(shù)時(shí)想對(duì)“this
”指定的值,也就是說,你可以重新指定你調(diào)用的函數(shù)里所有“this
”指向的對(duì)象。其他的變量指明了所有目標(biāo)函數(shù)運(yùn)行時(shí)接受的參數(shù)。
Note: In this case we specify the inherited properties when we create a new object instance, but note that you'll need to specify them as parameters in the constructor even if the instance doesn't require them to be specified as parameters (Maybe you've got a property that's set to a random value when the object is created, for example.)
注意:在這個(gè)例子里我們?cè)趧?chuàng)建一個(gè)新的對(duì)象實(shí)例時(shí)同時(shí)指派了繼承的所有屬性,但是注意你需要在構(gòu)造器里將它們作為參數(shù)來指派,即使實(shí)例不要求它們被作為參數(shù)指派(比如也許你在創(chuàng)建對(duì)象的時(shí)候已經(jīng)得到了一個(gè)設(shè)置為任意值的屬性)
所以在這個(gè)例子里,我們很有效的在Teacher()
構(gòu)造函數(shù)里運(yùn)行了Person()
構(gòu)造函數(shù)(見上文),得到了和在Teacher()
里定義的一樣的屬性,但是用的是傳送給Teacher()
,而不是Person()
的值(我們簡(jiǎn)單使用這里的this
作為傳給call()
的this
,意味著this
指向Teacher()
函數(shù))。
在構(gòu)造器里的最后一行代碼簡(jiǎn)單地定義了一個(gè)新的subject
屬性,這將是教師會(huì)有的,而一般人沒有的屬性。
順便提一下,我們本也可以這么做:
function Teacher(first, last, age, gender, interests, subject) { this.name = { first, last }; this.age = age; this.gender = gender; this.interests = interests; this.subject = subject;}
但是這只是重新定義了一遍屬性,并不是將他們從Person()中繼承過來的,所以這違背了我們的初衷。這樣寫也會(huì)需要更長的代碼。
Note that if the constructor you are inheriting from doesn't take its property values from parameters, you don't need to specify them as additional arguments in call()
. So, for example, if you had something really simple like this:
function Brick() { this.width = 10; this.height = 20;}
You could inherit the width
and height
properties by doing this (as well as the other steps described below, of course):
function BlueGlassBrick() { Brick.call(this); this.opacity = 0.5; this.color = 'blue';}
Note that we've only specified this
inside call()
— no other parameters are required as we are not inheriting any parameters, only properties.
到目前為止一切看起來都還行,但是我們遇到問題了。我們已經(jīng)定義了一個(gè)新的構(gòu)造器,這個(gè)構(gòu)造器默認(rèn)有一個(gè)空的原型屬性。我們需要讓Teacher()
從Person()
的原型對(duì)象里繼承方法。我們要怎么做呢?
Teacher.prototype = Object.create(Person.prototype);
create()
又來幫忙了——在這個(gè)例子里我們用這個(gè)函數(shù)來創(chuàng)建一個(gè)和Person.prototype
一樣的新的原型屬性值(這個(gè)屬性指向一個(gè)包括屬性和方法的對(duì)象),然后將其作為Teacher.prototype
的屬性值。這意味著Teacher.prototype
現(xiàn)在會(huì)繼承Person.prototype
的所有屬性和方法。Teacher()
的prototype
的constructor
屬性指向的是Person()
, 這是因?yàn)槲覀兩?code>Teacher()的方式?jīng)Q定的。(這篇 Stack Overflow post 文章會(huì)告訴你詳細(xì)的原理) — 將你寫的頁面在瀏覽器中打開,進(jìn)入JavaScript控制臺(tái),輸入以下代碼來確認(rèn): Teacher.prototype.constructor
Teacher.prototype.constructor = Teacher;
Teacher.prototype.constructor
就會(huì)得到Teacher()
。譯者注:每一個(gè)函數(shù)對(duì)象(Function
)都有一個(gè)prototype
屬性,并且只有函數(shù)對(duì)象有prototype
屬性,因?yàn)?code>prototype本身就是定義在Function
對(duì)象下的屬性。當(dāng)我們輸入類似var person1=new Person(...)
來構(gòu)造對(duì)象時(shí),Javascript實(shí)際上參考的是Person.prototype指向的對(duì)象來生成person1
。另一方面,Person()
函數(shù)是Person.prototype
的構(gòu)造函數(shù),也就是說Person===Person.prototype.constructor
(不信的話可以試試)。
在定義新的構(gòu)造函數(shù)Teacher
時(shí),我們通過function.call
來調(diào)用父類的構(gòu)造函數(shù),但是這樣無法自動(dòng)指定Teacher.prototype
的值,這樣Teacher.prototype
就只能包含在構(gòu)造函數(shù)里構(gòu)造的屬性,而沒有方法。因此我們利用Create
方法將Person
的原型對(duì)象復(fù)制給Teacher
的原型對(duì)象,并改變其構(gòu)造器指向,使之與Teacher
關(guān)聯(lián)。
任何你想要被繼承的方法都應(yīng)該定義在構(gòu)造函數(shù)的prototype
對(duì)象里,并且永遠(yuǎn)使用父類的prototype
來創(chuàng)造子類的prototype
,這樣才不會(huì)打亂類繼承結(jié)構(gòu)。
為了完善代碼,你還需在構(gòu)造函數(shù)Teacher()上定義一個(gè)新的函數(shù)greeting()。最簡(jiǎn)單的方法是在Teacher的原型上定義它—把以下代碼添加到你代碼的底部:
Teacher.prototype.greeting = function() { var prefix; if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') { prefix = 'Mr.'; } else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') { prefix = 'Mrs.'; } else { prefix = 'Mx.'; } alert('Hello. My name is ' + prefix + ' ' + this.name.last + ', and I teach ' + this.subject + '.');};
這樣就會(huì)出現(xiàn)老師打招呼的彈窗,老師打招呼會(huì)使用條件結(jié)構(gòu)判斷性別從而使用正確的稱呼。
現(xiàn)在我們來鍵入代碼,將下面的代碼放到你的 JavaScript 代碼下面從而來創(chuàng)建一個(gè) Teacher ( ) 對(duì)象實(shí)例。
var teacher1 = new Teacher('Dave', 'Griffiths', 31, 'male', ['football', 'cookery'], 'mathematics');
當(dāng)你保存代碼并刷新的時(shí)候,試一下你的老師實(shí)例的屬性和方法:
teacher1.name.first;teacher1.interests[0];teacher1.bio();teacher1.subject;teacher1.greeting();
前面三個(gè)進(jìn)入到從 Person() constructor 繼承的屬性和方法,后面兩個(gè)則是只有 Teacher() constrcuort 才有的屬性和方法。
Note: If you have trouble getting this to work, compare your code to our finished version (see it running live also).
我們?cè)谶@里講述的技巧并不是 JavaScript 中創(chuàng)建繼承類的唯一方式,但是這個(gè)技巧也還不錯(cuò),非常好地告訴了你如何在 JavaScript 中實(shí)行繼承操作。
你可能對(duì)在 JavaScript中使用其他方法來實(shí)行繼承會(huì)感興趣(參見 Classes)。我們沒有覆蓋那些內(nèi)容,因?yàn)椴⒉皇敲糠N瀏覽器都會(huì)支持這些方法。我們?cè)谶@一系列文章中介紹的所有其他方法都會(huì)被 IE9 支持或者更老的瀏覽器支持,也有一些方法可以支持更老的瀏覽器。
一個(gè)常用的方法是使用 JavaScript 語言庫——最熱門的一些庫提供一些方法讓我們更快更好地實(shí)行繼承。比如 CoffeeScript 就提供一些類和擴(kuò)展。
更多練習(xí)
在我們的 OOP theory section 模塊中, 我們也將學(xué)生類作為一個(gè)概念,繼承了 Person 所有的屬性和方法,也有一個(gè)不同的打招呼的方法(比老師的打招呼輕松隨意一些)。你可以自己嘗試一下如何實(shí)現(xiàn)。
Note: If you have trouble getting this to work, have a look at our finished version (see it running live also).
對(duì)象成員總結(jié)
總結(jié)一下,你應(yīng)該基本了解了以下三種屬性或者方法:
Those defined inside a constructor function that are given to object instances. These are fairly easy to spot — in your own custom code, they are the members defined inside a constructor using the this.x = x
type lines; in built in browser code, they are the members only available to object instances (usually created by calling a constructor using the new
keyword, e.g. var myInstance = new myConstructor()
).
Those defined directly on the constructor themselves, that are available only on the constructor. These are commonly only available on built-in browser objects, and are recognized by being chained directly onto a constructor, not a instance. For example, Object.keys()
.
Those defined on a constructor's prototype, which are inherited by all instances and inheriting object classes. These include any member defined on a Constructor's prototype property, e.g. myConstructor.prototype.x()
.
如果你現(xiàn)在覺得一團(tuán)漿糊,別擔(dān)心——你現(xiàn)在還處于學(xué)習(xí)階段,不斷練習(xí)才會(huì)慢慢熟悉這些知識(shí)。
特別是在讀完這段文章內(nèi)容之后,你也許會(huì)想 '天啊,這實(shí)在是太復(fù)雜了'. 是的,你是對(duì)的,原型和繼承代表了Javascript這門語言里最復(fù)雜的一些方面,但是Javascript的強(qiáng)大和靈活性正是來自于它的對(duì)象體系和繼承方式,這很值得花時(shí)間去好好理解下它是如何工作的。
在某種程度上來說,你一直都在使用繼承 - 無論你是使用WebAPI的不同特性還是調(diào)用字符串、數(shù)組等瀏覽器內(nèi)置對(duì)象的方法和屬性的時(shí)候
,你都在隱式地使用繼承。
就在自己代碼中使用繼承而言,你可能不會(huì)使用的非常頻繁,特定是在小型項(xiàng)目中或者剛開始學(xué)習(xí)時(shí) - 因?yàn)楫?dāng)你不需要對(duì)象和繼承的時(shí)候,僅僅為了使用而使用它們只是在浪費(fèi)時(shí)間而已。但是隨著你的代碼量的增大,你會(huì)越來越發(fā)現(xiàn)它的必要性。如果你開始創(chuàng)建一系列擁有相似特性的對(duì)象時(shí),那么創(chuàng)建一個(gè)包含所有共有功能的通用對(duì)象,然后在更特殊的對(duì)象類型中繼承這些特性,將會(huì)變得更加方便有用。
譯者注:
考慮到Javascript的工作方式,由于原型鏈等特性的存在,在不同對(duì)象之間功能的共享通常被叫做 委托 - 特殊的對(duì)象將功能委托給通用的對(duì)象類型完成。這也許比將其稱之為繼承更為貼切,因?yàn)椤氨焕^承”了的功能并沒有被拷貝到正在“進(jìn)行繼承”的對(duì)象中,相反它仍存在于通用的對(duì)象中。
在使用繼承時(shí),建議你不要使用過多層次的繼承,并仔細(xì)追蹤定義方法和屬性的位置。很有可能你的代碼會(huì)臨時(shí)修改了瀏覽器內(nèi)置對(duì)象的原型,但你不應(yīng)該這么做,除非你有足夠充分的理由。過多的繼承會(huì)在調(diào)試代碼時(shí)給你帶來無盡的混亂和痛苦。
總之,對(duì)象是另一種形式的代碼重用,就像函數(shù)和循環(huán)一樣,有他們特定的角色和優(yōu)點(diǎn)。如果你發(fā)現(xiàn)自己創(chuàng)建了一堆相關(guān)的變量和函數(shù)
,還想一起追蹤它們并將其靈活打包的話,對(duì)象是個(gè)不錯(cuò)的主意。對(duì)象在你打算把一個(gè)數(shù)據(jù)集合從一個(gè)地方傳遞到另一個(gè)地方的時(shí)候非常有用。這些都可以在不使用構(gòu)造器和繼承的情況下完成。如果你只是需要一個(gè)單一的對(duì)象實(shí)例,也許使用對(duì)象常量會(huì)好些,你當(dāng)然不需要使用繼承。
總結(jié)
這篇文章覆蓋了剩余的 OOJS 理論的核心知識(shí)和我們認(rèn)為你應(yīng)該知道的語法,這個(gè)時(shí)候你應(yīng)該理解了 JavaScript 中的對(duì)象和 OOP 基礎(chǔ),原型和原型繼承機(jī)制,如何創(chuàng)建類(constructors)和對(duì)象實(shí)例,為類增加功能,通過從其他類繼承而創(chuàng)建新的子類。
下一篇文章我們將學(xué)習(xí)如何運(yùn)用 JavaScript Object Notation (JSON), 一種使用 JavaScript 對(duì)象寫的數(shù)據(jù)傳輸格式。
另見
ObjectPlayground.com — A really useful interactive learning site for learning about objects.
Secrets of the JavaScript Ninja, Chapter 6 — A good book on advanced JavaScript concepts and techniques, by John Resig and Bear Bibeault. Chapter 6 covers aspects of prototypes and inheritance really well; you can probably track down a print or online copy fairly easily.
You Don't Know JS: this & Object Prototypes — Part of Kyle Simpson's excellent series of JavaScript manuals, Chapter 5 in particular looks at prototypes in much more detail than we do here. We've presented a simplified view in this series of articles aimed at beginners, whereas Kyle goes into great depth and provides a more complex but more accurate picture.
聯(lián)系客服