預(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 對(duì)象原型、原型鏈如何工作、如何向 prototype 屬性添加新的方法。 |
JavaScript 常被描述為一種基于原型的語(yǔ)言 (prototype-based language)——每個(gè)對(duì)象擁有一個(gè)原型對(duì)象,對(duì)象以其原型為模板、從原型繼承方法和屬性。原型對(duì)象也可能擁有原型,并從中繼承方法和屬性,一層一層、以此類推。這種關(guān)系常被稱為原型鏈 (prototype chain),它解釋了為何一個(gè)對(duì)象會(huì)擁有定義在其他對(duì)象中的屬性和方法。
準(zhǔn)確地說(shuō),這些屬性和方法定義在 Object 的構(gòu)造器函數(shù)之上,而非對(duì)象實(shí)例本身。
在傳統(tǒng)的 OOP 中,首先定義"類",此后創(chuàng)建對(duì)象實(shí)例時(shí),類中定義的所有屬性和方法都被復(fù)制到實(shí)例中。在 JavaScript 中并不如此復(fù)制——而是在對(duì)象實(shí)例和它的構(gòu)造器之間建立一個(gè)連接(作為原型鏈中的一節(jié)),以后通過(guò)上溯原型鏈,在構(gòu)造器中找到這些屬性和方法。
以上描述很抽象;我們先看一個(gè)例子。
讓我們回到 Person()
構(gòu)造器的例子。請(qǐng)把這個(gè)例子載入瀏覽器。如果你還沒(méi)有看完上一篇文章并寫好這個(gè)例子,也可以使用 oojs-class-further-exercises.html 中的例子(亦可參考源代碼)。
本例中我們將定義一個(gè)構(gòu)造器函數(shù):
function Person(first, last, age, gender, interests) { // 屬性與方法定義 };
然后創(chuàng)建一個(gè)對(duì)象實(shí)例:
var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
在 JavaScript 控制臺(tái)輸入 "person1.
",你會(huì)看到,瀏覽器將根據(jù)這個(gè)對(duì)象的可用的成員名稱進(jìn)行自動(dòng)補(bǔ)全:
在這個(gè)列表中,你可以看到定義在 person1
的原型對(duì)象、即 Person()
構(gòu)造器中的成員—— name
、age
、gender
、interests
、bio
、greeting
。同時(shí)也有一些其他成員—— watch
、valueOf
等等——這些成員定義在 Person()
構(gòu)造器的原型對(duì)象、即 Object
之上。下圖展示了原型鏈的運(yùn)作機(jī)制。
0px auto; width:700px;“>
那么,調(diào)用 person1
的"實(shí)際定義在 Object
上"的方法時(shí),會(huì)發(fā)生什么?比如:
person1.valueOf()
這個(gè)方法僅僅返回了被調(diào)用對(duì)象的值。在這個(gè)例子中發(fā)生了如下過(guò)程:
person1
對(duì)象是否具有可用的 valueOf()
方法。person1
對(duì)象的原型對(duì)象(即 Person
)是否具有可用的 valueof()
方法。Person()
構(gòu)造器的原型對(duì)象(即 Object
)是否具有可用的 valueOf()
方法。Object
具有這個(gè)方法,于是該方法被調(diào)用,注意:必須重申,方法和屬性沒(méi)有被復(fù)制到原型鏈中的其他對(duì)象——它們只是通過(guò)前述的"上溯原型鏈"的方式訪問(wèn)。
注意:沒(méi)有正式的方法用于直接訪問(wèn)一個(gè)對(duì)象的原型對(duì)象——原型鏈中的"連接"被定義在一個(gè)內(nèi)部屬性中,在 JavaScript 語(yǔ)言標(biāo)準(zhǔn)中用 [[prototype]]
表示(參見 ECMAScript)。然而,大多數(shù)現(xiàn)代瀏覽器還是提供了一個(gè)名為 __proto__
(前后各有2個(gè)下劃線)的屬性,其包含了對(duì)象的原型。你可以嘗試輸入 person1.__proto__
和 person1.__proto__.__proto__
,看看代碼中的原型鏈?zhǔn)鞘裁礃拥模?/p>
那么,那些繼承的屬性和方法在哪兒定義呢?如果你查看 Object
參考頁(yè),會(huì)發(fā)現(xiàn)左側(cè)列出許多屬性和方法——大大超過(guò)我們?cè)?person1
對(duì)象中看到的繼承成員的數(shù)量。某些屬性或方法被繼承了,而另一些沒(méi)有——為什么呢?
原因在于,繼承的屬性和方法是定義在 prototype
屬性之上的(你可以稱之為子命名空間 (sub namespace) )——那些以 Object.prototype.
開頭的屬性,而非僅僅以 Object.
開頭的屬性。prototype
屬性的值是一個(gè)對(duì)象,我們希望被原型鏈下游的對(duì)象繼承的屬性和方法,都被儲(chǔ)存在其中。
于是 Object.prototype.watch()、
Object.prototype.valueOf()
等等成員,適用于任何繼承自 Object()
的對(duì)象類型,包括使用構(gòu)造器創(chuàng)建的新的對(duì)象實(shí)例。
Object.is()
、Object.keys()
,以及其他不在 prototype
對(duì)象內(nèi)的成員,不會(huì)被"對(duì)象實(shí)例"或"繼承自 Object()
的對(duì)象類型"所繼承。這些方法/屬性僅能被 Object()
構(gòu)造器自身使用。
注意:這看起來(lái)很奇怪——構(gòu)造器本身就是函數(shù),你怎么可能在構(gòu)造器這個(gè)函數(shù)中定義一個(gè)方法呢?其實(shí)函數(shù)也是一個(gè)對(duì)象類型,你可以查閱 Function()
構(gòu)造器的參考文檔以確認(rèn)這一點(diǎn)。
prototype
屬性?;氐较惹暗睦?,在 JavaScript 控制臺(tái)輸入:
Person.prototype
prototype
屬性初始為空白?,F(xiàn)在嘗試:
Object.prototype
你會(huì)看到 Object
的 prototype
屬性上定義了大量的方法;如前所示,繼承自 Object
的對(duì)象都可以使用這些方法。
JavaScript 中到處都是通過(guò)原型鏈繼承的例子。比如,你可以嘗試從 String
、Date
、Number
和 Array
全局對(duì)象的原型中尋找方法和屬性。它們都在原型上定義了一些方法,因此當(dāng)你創(chuàng)建一個(gè)字符串時(shí):
var myString = 'This is my string.';
myString
立即具有了一些有用的方法,如 split()
、indexOf()
、replace()
等。
重要:prototype
屬性大概是 JavaScript 中最容易混淆的名稱之一。你可能會(huì)認(rèn)為,這個(gè)屬性指向當(dāng)前對(duì)象的原型對(duì)象,其實(shí)不是(還記得么?原型對(duì)象是一個(gè)內(nèi)部對(duì)象,應(yīng)當(dāng)使用 __proto__
訪問(wèn))。prototype
屬性包含(指向)一個(gè)對(duì)象,你在這個(gè)對(duì)象中定義需要被繼承的成員。
我們?cè)?jīng)講過(guò)如何用 Object.create()
方法創(chuàng)建新的對(duì)象實(shí)例。
var person2 = Object.create(person1);
create()
實(shí)際做的是從指定原型對(duì)象創(chuàng)建一個(gè)新的對(duì)象。這里以 person1
為原型對(duì)象創(chuàng)建了 person2
對(duì)象。在控制臺(tái)輸入:
person2.__proto__
結(jié)果返回 person1
對(duì)象。
每個(gè)對(duì)象實(shí)例都具有 constructor
屬性,它指向創(chuàng)建該實(shí)例的構(gòu)造器函數(shù)。
person1.constructor person2.constructor
都將返回 Person()
構(gòu)造器,因?yàn)樵摌?gòu)造器包含這些實(shí)例的原始定義。
一個(gè)小技巧是,你可以在 constructor
屬性的末尾添加一對(duì)圓括號(hào)(括號(hào)中包含所需的參數(shù)),從而用這個(gè)構(gòu)造器創(chuàng)建另一個(gè)對(duì)象實(shí)例。畢竟構(gòu)造器是一個(gè)函數(shù),故可以通過(guò)圓括號(hào)調(diào)用;只需在前面添加 new
關(guān)鍵字,便能將此函數(shù)作為構(gòu)造器使用。
var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']);
person3.name.first person3.age person3.bio()
正常工作。通常你不會(huì)去用這種方法創(chuàng)建新的實(shí)例;但如果你剛好因?yàn)槟承┰驔](méi)有原始構(gòu)造器的引用,那么這種方法就很有用了。
此外,constructor
屬性還有其他用途。比如,想要獲得某個(gè)對(duì)象實(shí)例的構(gòu)造器的名字,可以這么用:
instanceName.constructor.name
具體地,像這樣:
person1.constructor.name
從我們從下面這個(gè)例子來(lái)看一下如何修改構(gòu)造器的 prototype
屬性。
prototype
屬性添加一個(gè)新的方法:
Person.prototype.farewell = function() { alert(this.name.first + ' has left the building. Bye for now!'); }
person1.farewell();
你會(huì)看到一條警告信息,其中還顯示了構(gòu)造器中定義的人名;這很有用。但更關(guān)鍵的是,整條繼承鏈動(dòng)態(tài)地更新了,任何由此構(gòu)造器創(chuàng)建的對(duì)象實(shí)例都自動(dòng)獲得了這個(gè)方法。
再想一想這個(gè)過(guò)程。我們的代碼中定義了構(gòu)造器,然后用這個(gè)構(gòu)造器創(chuàng)建了一個(gè)對(duì)象實(shí)例,此后向構(gòu)造器的 prototype
添加了一個(gè)新的方法:
function Person(first, last, age, gender, interests) { // 屬性與方法定義 }; var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']); Person.prototype.farewell = function() { alert(this.name.first + ' has left the building. Bye for now!'); }
但是 farewell()
方法仍然可用于 person1
對(duì)象實(shí)例——舊有對(duì)象實(shí)例的可用功能被自動(dòng)更新了。這證明了先前描述的原型鏈模型。這種繼承模型下,上游對(duì)象的方法不會(huì)復(fù)制到下游的對(duì)象實(shí)例中;下游對(duì)象本身雖然沒(méi)有定義這些方法,但瀏覽器會(huì)通過(guò)上溯原型鏈、從上游對(duì)象中找到它們。這種繼承模型提供了一個(gè)強(qiáng)大而可擴(kuò)展的功能系統(tǒng)。
注意:如果運(yùn)行樣例時(shí)遇到問(wèn)題,請(qǐng)參閱 oojs-class-prototype.html 樣例(也可查看即時(shí)運(yùn)行)。
你很少看到屬性定義在 prototype 屬性中,因?yàn)槿绱硕x不夠靈活。比如,你可以添加一個(gè)屬性:
Person.prototype.fullName = 'Bob Smith';
但這不夠靈活,因?yàn)槿藗兛赡懿唤羞@個(gè)名字。用 name.first
和 name.last
組成 fullName
會(huì)好很多:
Person.prototype.fullName = this.name.first + ' ' + this.name.last;
然而,這么做是無(wú)效的,因?yàn)楸纠?this
引用全局范圍,而非函數(shù)范圍。訪問(wèn)這個(gè)屬性只會(huì)得到 undefined undefined
。但這個(gè)語(yǔ)句若放在先前定義的 prototype
的方法中則有效,因?yàn)榇藭r(shí)語(yǔ)句位于函數(shù)范圍內(nèi),從而能夠成功地轉(zhuǎn)換為對(duì)象實(shí)例范圍。你可能會(huì)在 prototype
上定義常屬性 (constant property) (指那些你永遠(yuǎn)無(wú)需改變的屬性),但一般來(lái)說(shuō),在構(gòu)造器內(nèi)定義屬性更好。
譯者注:關(guān)于 this
關(guān)鍵字指代(引用)什么范圍/哪個(gè)對(duì)象,這個(gè)問(wèn)題超出了本文討論范圍。事實(shí)上,這個(gè)問(wèn)題有點(diǎn)復(fù)雜,如果現(xiàn)在你沒(méi)能理解,也不用擔(dān)心。
事實(shí)上,一種極其常見的對(duì)象定義模式是,在構(gòu)造器(函數(shù)體)中定義屬性、在 prototype
屬性上定義方法。如此,構(gòu)造器只包含屬性定義,而方法則分裝在不同的代碼塊,代碼更具可讀性。例如:
// 構(gòu)造器及其屬性定義 function Test(a,b,c,d) { // 屬性定義 }; // 定義第一個(gè)方法 Test.prototype.x = function () { ... } // 定義第二個(gè)方法 Test.prototype.y = function () { ... } // 等等……
在 Piotr Zalewa 的 school plan app 樣例中可以看到這種模式。
本文介紹了 JavaScript 對(duì)象原型,包括原型鏈如何允許對(duì)象之間繼承特性、prototype
屬性、如何通過(guò)它來(lái)向構(gòu)造器添加方法,以及其他有關(guān)主題。
下一篇文章中,我們將了解如何在兩個(gè)自定義的對(duì)象間實(shí)現(xiàn)功能的繼承。
更多建議: