前言
如果你覺(jué)得JS的繼承寫(xiě)起來(lái)特別費(fèi)勁,特別艱澀,特別不倫不類,我想說(shuō),我也有同感。尤其是作為一個(gè)學(xué)過(guò)Java的人,看到JS的繼承簡(jiǎn)直要崩潰。至于為什么JS的繼承讓人如此困惑,根源當(dāng)然在于JS本身的設(shè)計(jì),即半函數(shù)式編程,半面向?qū)ο蟆?duì)歷史感興趣的人可以參考以下文章,雖然不會(huì)有恍然大悟——“原來(lái)繼承可以這么寫(xiě)”的感覺(jué),至少可以讓你對(duì)于自己的困惑找到一絲安慰。
http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html
至于我寫(xiě)這篇文章的目的,當(dāng)然是記錄一下自己的思考過(guò)程(有些東西是看了別人的代碼,在試圖理解其寫(xiě)法的目的),方便以后回頭重溫,畢竟人的忘性是很大的,另一方面則是有緣人看到這篇文章,希望它多少能幫助你在思考繼承的問(wèn)題上少走一點(diǎn)彎路。
另外免責(zé)聲明:本文寫(xiě)的是自己的思考過(guò)程,雖然力求正確,不至于誤人子弟,但是難免有疏漏和錯(cuò)誤,請(qǐng)諒解。如果能通過(guò)評(píng)論指正出來(lái),十分感謝。
正文
1.關(guān)于原型和原型鏈
說(shuō)到JS的繼承,當(dāng)然離不開(kāi)原型和原型鏈,因?yàn)樗鼈儽旧砭褪菫榱顺槿?gòu)造函數(shù)的共通部分而存在的。
1-1.困惑點(diǎn)
在原型和原型鏈的相關(guān)的問(wèn)題中,很多人比較困惑的大概是以下幾個(gè)。
<1>constructor,prototype和__proto的關(guān)系
<2>Function instanceof Object 和Object instanceof Function的結(jié)果為什么都是true
<3>為什么所有的對(duì)象的原型最終都指向Object.prototype而不是Object
(這個(gè)問(wèn)題可能不是大多數(shù)人都有的,但是我自己對(duì)理解這一點(diǎn)很是費(fèi)了一番功夫。)
1-2.需要知道的點(diǎn)
接下來(lái)說(shuō)一說(shuō)關(guān)于原型和原型鏈需要知道的一些點(diǎn)
<1>通過(guò)構(gòu)造函數(shù)創(chuàng)建對(duì)象的內(nèi)部原理
如下面的代碼:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
}
var Tony = new Person('tony', 18);
其實(shí)通過(guò)new操作符創(chuàng)建對(duì)象的過(guò)程是這樣的。
function Person(name, age) {
//1.創(chuàng)建一個(gè)對(duì)象,用this指向它。 this = {};
//2.執(zhí)行以下方法
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
}
//3.return this
}
var Tony = new Person('tony', 18);
<2>原型是函數(shù)的屬性prototype指向的內(nèi)容,原型本身是對(duì)象
首先必須要聲明一點(diǎn),雖然在JS里一切皆對(duì)象,所以函數(shù)也是對(duì)象。但是方便起見(jiàn),說(shuō)函數(shù)的時(shí)候仍然指普通意義上的函數(shù),即通過(guò)function關(guān)鍵字聲明的類型,而說(shuō)對(duì)象的時(shí)候指的是帶一對(duì)兒大括號(hào)的類型。
說(shuō)到這里有必要提一下,函數(shù)本質(zhì)上是一個(gè)代碼塊,是一塊普通代碼的集合,所以函數(shù)內(nèi)部是一行行的執(zhí)行語(yǔ)句,語(yǔ)句之間用分號(hào)隔開(kāi)。而對(duì)象本質(zhì)上是屬性名值對(duì)兒的集合,屬性名和屬性值之間用冒號(hào)連接在一起,不同的屬性名值對(duì)兒之間用逗號(hào)隔開(kāi)。
函數(shù)身上才有prototype屬性,用以顯式地聲明一個(gè)函數(shù)的原型。而原型本身必須是一個(gè)對(duì)象。這一點(diǎn)可以通過(guò)簡(jiǎn)單的代碼得以驗(yàn)證,不再貼圖。
<3>聲明prototype的含義
如以下代碼:
Person.prototype.sayName = function () {
console.log(this.name);
}
function Person(name, age) {
this.name = name;
this.age = age;
}
var Tony = new Person('tony', 18);
這里給Person函數(shù)加了一個(gè)原型。當(dāng)通過(guò)new操作符創(chuàng)建Person對(duì)象的時(shí)候,會(huì)在構(gòu)造函數(shù)內(nèi)部創(chuàng)建空對(duì)象后,立刻在里面放了一個(gè)隱式屬性proto指向了Person的原型對(duì)象。
Person.prototype.sayName = function () {
console.log(this.name);
}
function Person(name, age) {
//this = {__proto__ : Person.prototype}
this.name = name;
this.age = age;
}
var Tony = new Person('tony', 18);
需要注意的是,這里雖然只是寫(xiě)空對(duì)象里放了一個(gè)隱式屬性proto指向Person.prototype,從本質(zhì)上來(lái)說(shuō),Person.prototype這個(gè)詞本身并不是一個(gè)變量,也不是一個(gè)對(duì)象,并沒(méi)有夾在Person對(duì)象和Person的原型中間。當(dāng)創(chuàng)建Person對(duì)象后,Tony里面有一個(gè)proto直接指向一個(gè)對(duì)象,即:
{
sayName : function() {console.log(this.name);},
constructor : function Person(name, age) {this.name=name;this.age=age},
__proto__ : Object.prototype
}
使用prototype關(guān)鍵字把一個(gè)對(duì)象聲明為一個(gè)函數(shù)的原型,只是
意味著通過(guò)該函數(shù)創(chuàng)建對(duì)象時(shí),該對(duì)象內(nèi)部會(huì)有一個(gè)隱式屬性proto指向這個(gè)原型。這一點(diǎn)很重要。
<4>不顯式地聲明一個(gè)構(gòu)造函數(shù)的原型地話,系統(tǒng)會(huì)構(gòu)建一個(gè)隱式的原型
如以下代碼:
function Son(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.getFullName = function () {
console.log(this.lastName + this.name);
}
}
var Datou = new Son('Datou', 18, 'scapegoat');
系統(tǒng)會(huì)隱式地為Son構(gòu)造函數(shù)創(chuàng)建一個(gè)原型對(duì)象
{
constructor : function Son() {...},
__proto__ : Object.prototype
}
這里還是想再?gòu)?qiáng)調(diào)一下,雖然我寫(xiě)了Object.prototype,千萬(wàn)不要把它當(dāng)成一個(gè)存在的變量或?qū)ο螅踔炼妓悴簧弦粋€(gè)“指向”,而只是一個(gè)“指代”,指代的是Object的原型對(duì)象本身,那個(gè)帶大括號(hào)的對(duì)象。只不過(guò)由于它內(nèi)部的代碼很多,也沒(méi)有一個(gè)名字,所以我用prototype指代了一下。總而言之,我不想讓你誤以為存在一個(gè)叫Son.prototype的中間變量和對(duì)象,從而形成以下錯(cuò)誤印象:
Datou.proto → Son.prototype → {(隱式創(chuàng)建的)原型對(duì)象} → Object.prototype → {一大堆系統(tǒng)提供方法的集合}
而真實(shí)的情況是:
Datou.proto → {(隱式創(chuàng)建的)原型對(duì)象} → {一大堆系統(tǒng)提供方法的集合}
<5>實(shí)例對(duì)象的constructor是從原型對(duì)象復(fù)制過(guò)來(lái)的
個(gè)人感覺(jué),和prototype以及proto相比,constructor在繼承中的作用不是很大。
再來(lái)一個(gè)聲明,在本文討論的范圍內(nèi),對(duì)象分為三種,即實(shí)例對(duì)象,原型對(duì)象和Object.prototype。實(shí)例對(duì)象指的是通過(guò)new操作符創(chuàng)建出來(lái)的對(duì)象,原型對(duì)象指的是通過(guò)prototype關(guān)鍵字聲明的對(duì)象,而Object.prototype,指的是哪個(gè)一對(duì)兒大括號(hào),里面有一堆系統(tǒng)自定義的方法的對(duì)象。
還以下面的代碼舉例:
function Son(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.getFullName = function () {
console.log(this.lastName + this.name);
}
}
var Datou = new Son('Datou', 18, 'scapegoat');
Datou這個(gè)對(duì)象里有一個(gè)constructor指向系統(tǒng)隱式創(chuàng)建的Son的原型這一點(diǎn)我們?cè)偈煜げ贿^(guò)了,以至于我們可能會(huì)忽略其實(shí)Datou內(nèi)部并沒(méi)有一個(gè)叫constructor的屬性,它只是通過(guò)隱式屬性proto調(diào)用的原型上的constructor。這一點(diǎn)通過(guò)在控制臺(tái)打印Datou.hasOwnProperty('constructor')返回結(jié)果為false得以驗(yàn)證。
結(jié)論就是只有原型對(duì)象上才有constructor屬性,把一個(gè)對(duì)象當(dāng)做是誰(shuí)的原型,這個(gè)原型對(duì)象的constructor就指向誰(shuí)。
<6>原型對(duì)象為什么最終都鏈接到Object.prototype上
以下面這個(gè)簡(jiǎn)化版的代碼舉例:
Son.prototype.sayName = function () {
console.log(this.name);
}
function Son(name) {
this.name = name;
}
var Datou = new Son('Datou');
很顯然,系統(tǒng)會(huì)首先創(chuàng)建一個(gè)Son的原型對(duì)象,即:
{
sayName : function() {console.log(this.name)},
constructor : function Son(name) {this.name=name},
__proto__ : Object.prototype
}
里面有我們自定義的函數(shù)屬性sayName,同時(shí)會(huì)有一個(gè)constructor指向Son函數(shù),最后還有一個(gè)proto指向Object的原型。
在這里你會(huì)很自然的想到兩點(diǎn)。第一點(diǎn)是這個(gè)原型對(duì)象雖然看上去是我們手動(dòng)通過(guò)字面量形式寫(xiě)出來(lái)的,但其實(shí)一定是通過(guò)new出來(lái)的所以它內(nèi)部才有一個(gè)隱式屬性__proto。第二點(diǎn)是既然proto指向Object的原型,那Son的原型對(duì)象一定是通過(guò)Object函數(shù)new出來(lái)的。
事實(shí)確實(shí)如此。通過(guò)字面量的形式創(chuàng)建對(duì)象跟通過(guò)new Object()的方式創(chuàng)建對(duì)象本質(zhì)上是一樣的。所以通過(guò)以下代碼創(chuàng)建Datou的過(guò)程,以一種比較全面的角度來(lái)解讀是下面這樣的:
- 執(zhí)行var obj = new Object();
1-1)this = {};
1-2)this.proto = {一大堆系統(tǒng)提供方法的集合,由于當(dāng)前對(duì)象在Object函數(shù)中創(chuàng)建,所以該隱式屬性指向Object的原型對(duì)象};
1-3)this.sayName = function() {console.log(this.name)};
1-4)由于聲明了prototype所以this.constuctor = function Son(name) {this.name=name};
1-5)return this - 執(zhí)行var Datou = new Son('Datou');
2-1)this = {};
2-2)this.proto={由于當(dāng)前對(duì)象在Son函數(shù)中創(chuàng)建,所以該隱式屬性指向Son的原型對(duì)象,即obj指代的對(duì)象}
2-3)this.name = 'Datou';
2-4)return this;
最終,Datou就代表了如下一個(gè)對(duì)象
{
name : "Datou",
__proto__ : {包含sayName方法的那個(gè)原型對(duì)象}
}
看到這兒,應(yīng)該就能明白為什么所有的對(duì)象最終都會(huì)連接到Object的原型上了。
而這種通過(guò)隱式屬性proto不斷往上找原型的鏈條就是我們通常意義上所說(shuō)的原型鏈。
2.JS繼承的實(shí)現(xiàn)方式
如果看這篇文章之前你已經(jīng)查詢過(guò)百度很多遍,想必一定看到過(guò)繼承實(shí)現(xiàn)方式的演變歷史。下面談?wù)勛约簩?duì)繼承的理解,為此我準(zhǔn)備了一個(gè)例子,按照這個(gè)例子把代碼寫(xiě)出來(lái),基本上就能學(xué)會(huì)JS的繼承了。
頂部有一個(gè)Animal函數(shù),它里面有name,food和eat三個(gè)屬性,其中eat屬性是一個(gè)方法。Cat函數(shù)和Dog函數(shù)分別繼承自Animal,而Cat函數(shù)還有一個(gè)自己的屬性catMouse,最后通過(guò)Cat函數(shù)創(chuàng)建加菲貓,通過(guò)Dog函數(shù)創(chuàng)建歐弟。通過(guò)加菲貓調(diào)用eat方法,執(zhí)行的結(jié)果是加菲貓正在吃千層面,通過(guò)加菲貓調(diào)用catchMouse方法的話則打印我抓到了一只老鼠。通過(guò)歐弟調(diào)用eat方法,執(zhí)行的結(jié)果是歐弟正在啃骨頭。
2-1.首先想到的寫(xiě)法
通過(guò)百度或者加入一點(diǎn)自己的思考,最開(kāi)始得出的寫(xiě)法可能是這樣的。
function Animal(name, food) {
this.name = name;
this.food = food;
this.eat = function () {
console.log(this.name + " is eatting " + this.food);
}
}
function Cat(name, food) {
Animal.call(this, name, food);
this.catchMouse = function () {
console.log("Hey, John! I got a big mouse!")
}
}
function Dog(name, food) {
Animal.call(this, name, food);
}
var Garfield = new Cat("Garfield", 'lasagne');
var Odie = new Dog("Odie", "bone");
Garfield.eat();
Garfield.catchMouse();
Odie.eat();
這種寫(xiě)法從功能實(shí)現(xiàn)的角度來(lái)說(shuō)已經(jīng)沒(méi)有問(wèn)題了,但是沒(méi)有用到原型很讓人不爽。怎么說(shuō)呢,之所以出現(xiàn)原型,就是為了提取共通的部分,這也是繼承的題中之義。上面這種寫(xiě)法,單純是使用Animal函數(shù)的call方法為自己初始化變量,其實(shí)本質(zhì)上是“借腹生子”。而且看起來(lái)是寫(xiě)的代碼少了,但實(shí)際上執(zhí)行的步驟可是一步都不少。而且,函數(shù)類的屬性一般都要寫(xiě)到原型里,不然要原型干嘛。像現(xiàn)在這種寫(xiě)法的話,相當(dāng)于每個(gè)Cat對(duì)象里都會(huì)存放一份eat函數(shù)和catchMouse函數(shù),而每個(gè)Dod對(duì)象里都會(huì)放一份eat函數(shù),這根本沒(méi)有顯示出繼承的特點(diǎn)。
2-2.加上原型后的效果
接下來(lái)把共通的部分抽取出來(lái)放到原型里,并用原型鏈鏈接起來(lái)。
Animal.prototype.eat = function () {
console.log(this.name + " is eatting " + this.food);
}
function Animal(name, food) {
this.name = name;
this.food = food;
}
Cat.prototype.catchMouse = function () {
console.log("Hey, John! I got a big mouse!")
}
Cat.prototype = new Animal();
function Cat(name, food) {
Animal.call(this, name, food);
}
Dog.prototype = new Animal();
function Dog(name, food) {
Animal.call(this, name, food);
}
var Garfield = new Cat("Garfield", 'lasagne');
var Odie = new Dog("Odie", "bone");
Garfield.eat();
Garfield.catchMouse();
Odie.eat();
這個(gè)時(shí)候就開(kāi)始遇到一個(gè)很嚴(yán)重的問(wèn)題。
假如沒(méi)有加菲貓,只有歐弟,即子函數(shù)的原型上沒(méi)有自己獨(dú)有的方法。
Animal.prototype.eat = function () {
console.log(this.name + " is eatting " + this.food);
}
function Animal(name, food) {
this.name = name;
this.food = food;
}
Dog.prototype = new Animal();
function Dog(name, food) {
Animal.call(this, name, food);
}
var Odie = new Dog("Odie", "bone");
Odie.eat();
這就沒(méi)問(wèn)題,因?yàn)镈og的原型是Animal對(duì)象,即Odie里面有一個(gè)隱式屬性proto指向一個(gè)Animal對(duì)象(它沒(méi)有名字),而Animal的原型是一個(gè)包含eat函數(shù)的對(duì)象,所以Animal對(duì)象里面有一個(gè)隱式屬性proto指向那個(gè)包含eat函數(shù)的對(duì)象。這樣的話,Odie調(diào)用eat方法的話,自己沒(méi)有,通過(guò)自己的proto找到那個(gè)Animal對(duì)象,結(jié)果它也沒(méi)有,再通過(guò)Animal對(duì)象的proto找到那個(gè)包含eat函數(shù)的對(duì)象,執(zhí)行其中的eat方法。
然而換種想法,其實(shí)這種情況下還是有一點(diǎn)值得思考的,就是為什么不直接寫(xiě)Dog.prototype = Animal.prototype呢。可以直接這么寫(xiě),而且直接寫(xiě)其實(shí)比中間通過(guò)一個(gè)Animal對(duì)象要好。這么寫(xiě)的話,Odie的proto直接指向包含eat方法的對(duì)象,正好就是自己想要的效果。剛才那種寫(xiě)法反而每次都創(chuàng)建出一個(gè)多余的Animal對(duì)象,里面有兩個(gè)屬于自己的屬性(沒(méi)有通過(guò)call改變this指向,通過(guò)new創(chuàng)建的時(shí)候可以寫(xiě)參數(shù)也可以不寫(xiě)參數(shù),一般不寫(xiě),沒(méi)必要),這兩個(gè)參數(shù)的值最終都是undefined。實(shí)際上這個(gè)對(duì)象唯一的作用就是里面有一個(gè)proto指向自己的構(gòu)造函數(shù)的原型。
Dog.prototype = Animal.prototype其實(shí)有一個(gè)專業(yè)的叫法——共享原型。但是它不是總能奏效,比如加菲貓這頭,它還需要有一個(gè)專屬于貓科動(dòng)物的特性——抓老鼠。
Cat既要有自己的一個(gè)原型(是一個(gè)對(duì)象),里面包含一個(gè)catchMouse方法,以便Garfield的proto指向這個(gè)原型,同時(shí)又要讓加菲貓的原型直接或間接地指向Animal的原型。以共享原型的方式直接指向的話是沒(méi)戲的,因?yàn)镃at.prototype=Animal.prototype的話就沒(méi)有中間的對(duì)象了,而我恰好需要一個(gè)中間的對(duì)象在這兒。以開(kāi)始的那種先執(zhí)行new Animal()對(duì)象再間接指向最終目標(biāo)的話,會(huì)出現(xiàn)以下3中結(jié)果。
1)先寫(xiě)Cat.prototype = new Animal();再寫(xiě)Cat.prototype={catchMouse:function() {...}}的話,前者會(huì)被后者覆蓋掉,導(dǎo)致最終沒(méi)法指向Animal的eat方法。
2)先寫(xiě)Cat.prototype = new Animal();再寫(xiě)Cat.prototype.catchMouse=function() {...}的話,每個(gè)創(chuàng)建出來(lái)的Cat對(duì)象里仍然會(huì)有一份無(wú)用的Cat對(duì)象的屬性值這一點(diǎn)并沒(méi)有變。而且明明是Cat的方法,卻寫(xiě)到了Animal對(duì)象里,語(yǔ)義上好像不好。
3)先寫(xiě)Cat.prototype.catchMouse=function() {...}再寫(xiě)Cat.prototype = new Animal();的話,前者會(huì)被后者覆蓋,導(dǎo)致實(shí)際上并沒(méi)有添加屬于自己的特有方法。
這個(gè)時(shí)候靜下心來(lái)想一想,自己到底想要達(dá)到什么效果?其實(shí)就是Cat函數(shù)的原型對(duì)象確實(shí)存在,它里面有一個(gè)Cat函數(shù)才有的catchMouse方法,這個(gè)原型對(duì)象里應(yīng)該有一個(gè)proto直接指向Animal的原型。但是就是辦不到。于是我手動(dòng)地在創(chuàng)建Cat的原型對(duì)象后,給他顯式地添加一個(gè)proto屬性,讓它指向Animal的原型,效果如下:
Animal.prototype.eat = function () {
console.log(this.name + " is eatting " + this.food);
}
function Animal(name, food) {
this.name = name;
this.food = food;
}
Cat.prototype.catchMouse = function () {
console.log("Hey, John! I got a big mouse!")
}
Cat.prototype.__proto__ = Animal.prototype;
function Cat(name, food) {
Animal.call(this, name, food);
}
var Garfield = new Cat("Garfield", 'lasagne');
Garfield.eat();
Garfield.catchMouse();
試了一下,結(jié)果居然是可以的。然而,IE瀏覽器下并不好使,其它瀏覽器倒是好使。
其實(shí)這個(gè)時(shí)候,退而求其次,既然看起來(lái)沒(méi)法同時(shí)做到這兩點(diǎn),把Cat函數(shù)的catchMouse屬性不用原型實(shí)現(xiàn),而是老老實(shí)實(shí)地寫(xiě)到函數(shù)內(nèi)部也還好。代碼重復(fù)就重復(fù)好了,不是徹底的繼承就不徹底好了,至少?gòu)墓δ苌蟻?lái)講也實(shí)現(xiàn)了要求。
Animal.prototype.eat = function () {
console.log(this.name + " is eatting " + this.food);
}
function Animal(name, food) {
this.name = name;
this.food = food;
}
Cat.prototype = Animal.prototype;
function Cat(name, food) {
Animal.call(this, name, food);
this.catchMouse = function () {
console.log("Hey, John! I got a big mouse!")
}
}
var Garfield = new Cat("Garfield", 'lasagne');
Garfield.eat();
Garfield.catchMouse();
退而求其次也行,但是這看起來(lái)是不可調(diào)和的矛盾,真的沒(méi)有辦法了么?答案是有的。
2-3.最后改進(jìn)后的結(jié)果
重新捋一下需求和思路。寫(xiě)到這種地步,Animal的非函數(shù)屬性(name和food)采用了一種非本質(zhì)上繼承,但是也還好的方式實(shí)現(xiàn)了,現(xiàn)在主要是想鏈接到Animal的原型上,獲取到eat方法。Cat函數(shù)要有自己的方法,所以屬于自己的原型對(duì)象必須存在。即Cat.prototype.catchMouse=function(){...}是一定有的,這個(gè)對(duì)象必須實(shí)實(shí)在在地存在。然后這個(gè)對(duì)象需要有一個(gè)proto鏈接到Animal的原型,強(qiáng)制加proto是不現(xiàn)實(shí)的。其實(shí)仔細(xì)想行,為什么讓這個(gè)原型對(duì)象里有一個(gè)proto直接指向Animal的原型那么難(其實(shí)是不可能的),這又回到開(kāi)頭的問(wèn)題上,顯式聲明prototype和proto是怎么回事兒,以及什么關(guān)系上。
使用prototype關(guān)鍵字把一個(gè)對(duì)象聲明為一個(gè)函數(shù)的原型,只是
意味著通過(guò)該函數(shù)創(chuàng)建對(duì)象時(shí),該對(duì)象內(nèi)部會(huì)有一個(gè)隱式屬性proto指向這個(gè)原型。
所以Animal的原型只會(huì)在new Animal()的對(duì)象里才會(huì)被proto引用。而Cat的原型,本身已經(jīng)是一個(gè)對(duì)象(里面有一個(gè)catchMouse函數(shù)屬性)了,它就不可能是Animal的對(duì)象。
既然我沒(méi)法直接指向你,而我所需要的又只是你(Animal的原型),而不是Animal對(duì)象(指向Animal對(duì)象會(huì)有多余的屬性)。既然無(wú)論如何都只能是間接地指向你,現(xiàn)有的那個(gè)我又不喜歡,那我干脆創(chuàng)建一個(gè)新的空對(duì)象作為間接內(nèi)容指向你好了。于是可以創(chuàng)建一個(gè)新的函數(shù)(內(nèi)容為空),讓這個(gè)函數(shù)的原型也是你,這個(gè)函數(shù)構(gòu)造出的對(duì)象里其它什么都沒(méi)有,只有一個(gè)proto指向你,然后我自己加我特有的內(nèi)容時(shí)加到這個(gè)新的對(duì)象上了。雖然仍然有語(yǔ)義上不好的感覺(jué),至少?zèng)]有多余的Animal對(duì)象的屬性了。
Animal.prototype.eat = function () {
console.log(this.name + " is eatting " + this.food);
}
function Animal(name, food) {
this.name = name;
this.food = food;
}
function F() {}
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.catchMouse = function () {
console.log("Hey, John! I got a big mouse!")
}
function Cat(name, food) {
Animal.call(this, name, food);
}
var Garfield = new Cat("Garfield", 'lasagne');
Garfield.eat();
Garfield.catchMouse();
其實(shí)這一塊代碼還可以設(shè)計(jì)成一個(gè)單獨(dú)的函數(shù):
function extend(son, father) {
function F() {};
F.prototype = father.prototype;
son.prototype = new F();
}
Animal.prototype.eat = function () {
console.log(this.name + " is eatting " + this.food);
}
function Animal(name, food) {
this.name = name;
this.food = food;
}
extend(Cat, Animal);
Cat.prototype.catchMouse = function () {
console.log("Hey, John! I got a big mouse!")
}
function Cat(name, food) {
Animal.call(this, name, food);
}
var Garfield = new Cat("Garfield", 'lasagne');
Garfield.eat();
Garfield.catchMouse();
這樣的寫(xiě)法已經(jīng)很好地完成了任務(wù)了。再錦上添花一下的話,需要思考一下constructor的事兒。從這里可以看出來(lái),constructor對(duì)應(yīng)JS的原型以及原型鏈來(lái)講作用遠(yuǎn)不及prototype和proto重要。但是為了更符合JS原型和原型鏈的體系,有必要再加點(diǎn)東西。
上面的代碼中,通過(guò)Garfield訪問(wèn)constructor屬性會(huì)打印出Animal函數(shù)。原因是Cat的prototype是新建的空對(duì)象,而那個(gè)空對(duì)象自己也沒(méi)有constructor,它的proto才有,它的proto是Animal的prototype,結(jié)果可想而知。所以為了,體現(xiàn)出Garfield是在Cat函數(shù)中創(chuàng)建出來(lái)地這一點(diǎn),有必要加上以下代碼:
son.prototype.constructor = son;
結(jié)果代碼如下:
function extend(son, father) {
function F() {};
F.prototype = father.prototype;
son.prototype = new F();
son.prototype.constructor = son;
}
Animal.prototype.eat = function () {
console.log(this.name + " is eatting " + this.food);
}
function Animal(name, food) {
this.name = name;
this.food = food;
}
extend(Cat, Animal);
Cat.prototype.catchMouse = function () {
console.log("Hey, John! I got a big mouse!")
}
function Cat(name, food) {
Animal.call(this, name, food);
}
var Garfield = new Cat("Garfield", 'lasagne');
Garfield.eat();
Garfield.catchMouse();