JavaScript - 繼承和類
在這一篇中,我要聊聊 JavaScript 中的繼承和“類”燕垃。
首先跟你請(qǐng)教下箭券,到底為啥要使用繼承和類呢?
在“面向?qū)ο蟆钡木幊填I(lǐng)域里狸涌,好像需要一個(gè)“對(duì)象”的時(shí)候切省,就聲明這個(gè)對(duì)象的 class,然后實(shí)例化這個(gè) class 來得到一個(gè)對(duì)象帕胆。這種做法貌似隨著面向?qū)ο蟮木幊陶Z言的廣泛使用朝捆,成了“標(biāo)準(zhǔn)”似的。在 Java懒豹、C# 這樣的編程語言中芙盘,和繼承以及類相關(guān)的有很多的概念驯用,形成了一個(gè)龐大、復(fù)雜的體系儒老。但是至少我在了解這些的時(shí)候沒有思考過蝴乔,到底是因?yàn)槭裁磳?dǎo)致我們需要這些東西呢?或者說驮樊,使用繼承和類薇正,是為了解決怎樣的根本性的問題呢?
(當(dāng)然囚衔,有些問題由于提問者本身認(rèn)知的局限性挖腰,可能本就不是值得回答的_)
有一種說法,是我在學(xué)習(xí) JavaScript 的過程中了解到的佳魔,說是繼承這個(gè)東西本意是為了“代碼復(fù)用”曙聂。我目前是比較認(rèn)同這種看法,不過沒有了解到更多的經(jīng)典說法鞠鲜,所以沒有更多的比較宁脊。或許從 Java 程序員的你這里贤姆,我可以學(xué)到更多榆苞,甚至在這個(gè)問題的看法上,我會(huì)有很大的改變也說不定霞捡。不過坐漏,限于目前的認(rèn)知情況,我就繼續(xù)按這個(gè)思路聊啦碧信。
創(chuàng)建一個(gè)抽象的類赊琳,將具體的屬性、方法綁定到這個(gè)類上砰碴,然后實(shí)例化得到的該類型的對(duì)象就擁有了這些屬性躏筏、方法。嗯呈枉,的確是很自然的趁尼。其中有個(gè)細(xì)節(jié),我特意說下猖辫,可能并不重要酥泞。就是,同一類的不同對(duì)象啃憎,方法是相同的芝囤,只是屬性值可能不同。因?yàn)閷傩灾悼梢钥醋鍪菍?duì)象攜帶的數(shù)據(jù),對(duì)于不同的對(duì)象而言是不同的凡人,如果完全相同名党,或許就沒有必要?jiǎng)?chuàng)建兩個(gè)對(duì)象了不是(當(dāng)然對(duì)于特殊的,如常量挠轴,就不是如此了)传睹。這個(gè)體現(xiàn)在 JavaScript 中的話,應(yīng)該是下面這個(gè)樣子:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
而不能是:
function Person(name) {
this.name = name;
this.getName = function () {
return this.name;
};
}
原因就在于岸晦,后面的這種方式下欧啤,每個(gè)以 new Person('xxx')
這種方式創(chuàng)建的對(duì)象,都有自己的 getName()
方法启上,而不是相同的方法邢隧。從對(duì)象屬性的這個(gè)角度來解釋,就是前一種方式下冈在,新創(chuàng)建的對(duì)象并沒有名稱為 getName
的屬性倒慧,而后一種有。但是前一種也能夠使用到這個(gè)方法包券,是因?yàn)榈玫降膶?duì)象“繼承”到了這個(gè)方法纫谅。下面就引出 JavaScript 中的繼承了。
JavaScript 中的繼承
JavaScript 中的繼承基于原型(prototype)的溅固。JavaScript 中沒有“類”的存在付秕,繼承是指一個(gè)對(duì)象從另一個(gè)對(duì)象那里繼承。由于可以一級(jí)級(jí)地繼承下去侍郭,所以會(huì)產(chǎn)生一個(gè)繼承鏈询吴。于是,在試圖獲取一個(gè)對(duì)象的屬性時(shí)亮元,如果這個(gè)對(duì)象本身沒有定義猛计,則會(huì)到它的原型對(duì)象那里去找找,再?zèng)]有的話就繼續(xù)往上一級(jí)的原型對(duì)象里查找爆捞,直到找到或達(dá)到最頂級(jí)的原型對(duì)象(具體是什么我不了解奉瘤,就不亂說了)那里。另外嵌削,當(dāng)前對(duì)象中定義了的屬性毛好,會(huì)“覆蓋”從原型對(duì)象那里繼承的屬性望艺。這一點(diǎn)不難理解苛秕,因?yàn)樵诋?dāng)前對(duì)象找到了就不會(huì)往上查找了嘛。不過由于 JavaScript 是動(dòng)態(tài)語言找默,執(zhí)行過程中可能會(huì)刪除對(duì)象的屬性艇劫,這個(gè)時(shí)候從原型對(duì)象中繼承的屬性就又會(huì)暴露出來了。這種“鏈”的機(jī)制惩激,和作用域鏈有點(diǎn)類似店煞,在函數(shù)中蟹演,一個(gè)變量名稱在當(dāng)前函數(shù)作用域下找不到的時(shí)候,就會(huì)往上一級(jí)查找顷蟀,而如果在當(dāng)前函數(shù)作用域中有定義酒请,則會(huì)覆蓋上級(jí)中的同名變量。
最清晰不過的話鸣个,應(yīng)該給一張圖羞反,我該用心畫一張,不過想想囤萤,還是推薦去看看書里的圖吧昼窗。
再次強(qiáng)調(diào)下,我理解的 JavaScript 中的繼承涛舍,就是一個(gè)對(duì)象到另一個(gè)對(duì)象澄惊,沒有類參與其中。
這種“原型”方式的繼承富雅,應(yīng)該是比較直觀和易理解的掸驱,不說更多了。
JavaScript 中的“類”
然而吹榴,JavaScript 這門擁有各種特性的語言里亭敢,還就是有“類”的身影。像上面的第一個(gè)例子里图筹,會(huì)涉及到幾個(gè)詞:構(gòu)造函數(shù)(constructor)帅刀、原型對(duì)象(prototype)、實(shí)例對(duì)象(instance)远剩。對(duì)扣溺,實(shí)際上沒有類,但是這些東西整體的作用瓜晤,給人一種定義了一個(gè)叫做“Person”的類的錯(cuò)覺锥余。
具體來說下上面的第一個(gè)例子。
首先說 Person
痢掠,它是一個(gè)首先是一個(gè)函數(shù)驱犹,這很明顯。只有被以 new Person(...)
這種方式使用時(shí)足画,我們才可以說它是一個(gè)構(gòu)造函數(shù)雄驹。為什么呢?因?yàn)檫@種方式通常情況下會(huì)返回一個(gè)新的實(shí)例對(duì)象淹辞,是誰的實(shí)例呢医舆?不是這個(gè)構(gòu)造函數(shù)的,而是這個(gè)構(gòu)造函數(shù)所參與構(gòu)造的這個(gè)看不到的,但是貌似存在的“類”蔬将。這么說是不是很難讓人明白爷速?
函數(shù)作為構(gòu)造函數(shù)被使用是,它的 prototype
屬性是有特殊的用途霞怀。在這種情況下惫东,這個(gè)屬性所指向的對(duì)象,會(huì)被作為得到的實(shí)例對(duì)象的原型對(duì)象使用毙石。也就是說凿蒜,通過構(gòu)造函數(shù)的 prototype
屬性來連接實(shí)例對(duì)象和它的原型對(duì)象,不過實(shí)際對(duì)于一個(gè)已經(jīng)存在的對(duì)象來說胁黑,并不需要這個(gè)構(gòu)造函數(shù)來持續(xù)維系這種關(guān)聯(lián)關(guān)系废封。對(duì)象可以直接找到它的原型對(duì)象(如果有的話),有的運(yùn)行環(huán)境下還提供了一些特殊的屬性丧蘸、方法來做這些事情漂洋,這個(gè)推薦大家看相關(guān)資料,我就不亂說了力喷。
原型對(duì)象其實(shí)還可以通過 constructor
屬性來關(guān)聯(lián)對(duì)應(yīng)的構(gòu)造函數(shù)刽漂,不過這對(duì)于繼承這件事情來說并不是必須,而且很多時(shí)候甚至沒有這種屬性弟孟,例如上面的例子中如果是以明確的對(duì)象來給出原型對(duì)象的話:
Person.prototype = {
getName: function () {
return this.name;
}
};
這個(gè)直接聲明的對(duì)象顯然沒有 constructor
屬性贝咙,所以最上面的例子中,其實(shí)隱含著一個(gè)已經(jīng)有的原型對(duì)象(Person.prototype
)拂募,只不過是向這個(gè)原型對(duì)象中設(shè)置了新的屬性而已庭猩。然而這里的用法就是給 Person
的 prototype
屬性指定了新的對(duì)象了,所以還是不同的陈症。
另外蔼水,JavaScript 中可以用 obj instanceof class
來判斷一個(gè)對(duì)象是否為“類”(當(dāng)然這里的 class 其實(shí)是構(gòu)造函數(shù))的實(shí)例。仔細(xì)研究下這個(gè) instanceof
還是會(huì)有些收獲的录肯,這里推薦去看下相關(guān)資料(在 MDN 搜索下吧)趴腋。
綜上,構(gòu)造函數(shù)论咏、原型對(duì)象优炬,再加上使用 new Constructor(...)
這種方式來獲得實(shí)例對(duì)象,一起構(gòu)造了一個(gè)“偽類”的機(jī)制厅贪,使得初次看到這個(gè)的 Java 程序員們可能因?yàn)槭煜ざ暨M(jìn)了這個(gè)“坑”里蠢护。
有一點(diǎn)需要注意,在最上面的例子中卦溢,雖然看起來定義了一個(gè)類糊余,但如果使用方式不當(dāng),還是會(huì)有問題的单寂,例如:
var me = Person('luobo');
me.getName(); // 報(bào)錯(cuò)贬芥!
這里之所以會(huì)報(bào)錯(cuò),是因?yàn)闆]有用 new Person('luobo')
這種形式來創(chuàng)建對(duì)象宣决。因?yàn)?Person
只是一個(gè)普通函數(shù)蘸劈,如果沒有以 new ...
的方式來使用的,就是一個(gè)普通的函數(shù)調(diào)用而已尊沸。特別地威沫,因?yàn)樵?Person
中這樣寫著:
this.name = name;
此時(shí),由于 this
并沒有指定為特定的對(duì)象洼专,所以可能會(huì)被設(shè)置為全局對(duì)象(瀏覽器下面的 window 對(duì)象)棒掠,因而可能成了給全局對(duì)象添加屬性!
這一切并不會(huì)因?yàn)椤帮@式”地給函數(shù)名稱首字母大寫屁商,并“顯式”操作了函數(shù)的 prototype
屬性而有所不同烟很。如果想避免這種情況,防止構(gòu)造函數(shù)使用時(shí)忘了加 new
出現(xiàn)問題蜡镶,可以這樣:
function Person(name) {
if (!(this instanceof Person) {
return new Person(name);
}
this.name = name;
}
還可以將真正的構(gòu)造函數(shù)另外定義雾袱,例如:
function Person(name) {
return new Person.init(name);
}
Person.init = function (name) {
this.name = name;
};
jQuery 使用的是類似這種方式,另外還將 jQuery.prototype
暴露為 jQuery.fn
官还,以方便對(duì)原型對(duì)象進(jìn)行操作(例如插件擴(kuò)展時(shí)就可以:$.fn.pluginName = ...
):
var jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context );
};
jQuery.fn = jQuery.prototype = { /* ... */ }
var init = jQuery.fn.init = function( selector, context ) { /* ... */ }
init.prototype = jQuery.fn;
上面是從 jQuery 源碼中摘出來的一部分芹橡。
回到最初
盡管在 JavaScript 中有這種模仿類的機(jī)制,而且也在實(shí)踐中被使用著望伦。但如果只是為了解決“代碼復(fù)用”的問題林说,這并不是唯一的方法,也不見得是最好的屯伞。在一些書中會(huì)有更多述么、更詳盡的討論,感興趣可以找來看看愕掏。我想關(guān)于這個(gè)問題度秘,主要還是由于“函數(shù)”在 JavaScript 中的特殊地位造成的,從復(fù)用方法這個(gè)角度來說饵撑,任何方法基本上都可以被復(fù)用剑梳,甚至根本不需要借助“繼承”來實(shí)現(xiàn)。例如:
function foo(name) {
// 獲得除 name 外其他在函數(shù)調(diào)用時(shí)傳入的參數(shù)
// 例如 foo('luobo', 1, 'abc') ==> otherArgs: [1, 'abc']
// 由于 arguments 并非數(shù)組對(duì)象滑潘,沒有截取部分元素的方法垢乙,
// 這里借助數(shù)組對(duì)象的 slice 方法來實(shí)現(xiàn)
var otherArgs = [].slice.call(arguments, 1);
// ...
}
好了,就寫到這吧语卤。