javascript繼承之為什么要繼承
更新時(shí)間:2012年11月10日 16:10:10 作者:
本文詳細(xì)介紹javascript的繼承等各方面相關(guān)知識(shí)
Quiz1
Javascript真的需要類(lèi)(Class)么?
我們首先先看下其他有類(lèi)(Class)的面向?qū)ο笳Z(yǔ)言(如:Java)的一些特性。
父類(lèi)與子類(lèi)
父類(lèi)(Superclass)和子類(lèi)(Subclass),并不是為了解決父親與兒子的問(wèn)題,而是為了解決類(lèi)的包含關(guān)系的,我們用Sub表示“子類(lèi)”,用Sup表示“父類(lèi)”,則有:
Sub Sup
這是有區(qū)別的,例如通常我們能夠?qū)⒆宇?lèi)當(dāng)成父類(lèi)來(lái)使用,但認(rèn)人的時(shí)候我們并不能把兒子當(dāng)成父親。
或者可以這么說(shuō),父類(lèi)和子類(lèi)不是為了解決類(lèi)間存在相同方法或者屬性的。
舉個(gè)例子
有人喜歡這樣做:
我們需要一些動(dòng)物的類(lèi),以便在屏幕上創(chuàng)建一些移動(dòng)的動(dòng)物,但移動(dòng)的動(dòng)物有些在空中飛行,有些在路上行走。
所以創(chuàng)建兩個(gè)父類(lèi),一個(gè)是Fly,一個(gè)是Walk:
Class Fly{
Fly(){}
}
Class Walk{
Walk(){}
}
然后獅子們(還可以再建些其他的在路上行走的動(dòng)物)就屬于Walk類(lèi),老鷹們(也還可以再建些其他在天上飛行的動(dòng)物)就屬于Fly類(lèi):
Class Lion extend Walk{
}
Class Eagle extend Fly{
}
最后對(duì)Lion和Eagle類(lèi)創(chuàng)建一些實(shí)例,調(diào)用相應(yīng)的方法,屏幕上就會(huì)有一些獅子和老鷹在移動(dòng)了。
但這可能并不是一個(gè)好的設(shè)計(jì),比如明天老板突然一拍大腦,他要有一種叫天馬(Pegasus)的動(dòng)物,它們即會(huì)在天上飛,又會(huì)在路上走,時(shí)而要飛行,時(shí)候要行走。
在這種情況下,這個(gè)方案就全然無(wú)用了。
為什么這個(gè)設(shè)計(jì)失敗了?
繼承是有條件的,子類(lèi)必須能?chē)?yán)格的向上轉(zhuǎn)型(變成父類(lèi))。
在上面這個(gè)例子中:
獅子(Lion)被假設(shè)等同于行走動(dòng)物(Walk),老鷹(Eagle)被假設(shè)等同于飛行動(dòng)物(Fly)。
這看起來(lái)很成功,因?yàn)樽宇?lèi)能?chē)?yán)格向上轉(zhuǎn)型,但他有隱患。
當(dāng)有一種天馬(Pegasus)介入到里面的時(shí)候,我們才發(fā)現(xiàn)獅子其實(shí)只是“會(huì)行走的動(dòng)物”,老鷹其實(shí)只是“會(huì)飛行的動(dòng)物”,這不意味著動(dòng)物一輩子只能飛行或者行走,所以即會(huì)飛行又會(huì)行走的天馬就找不到自己的歸屬了。
這個(gè)例子很好的證明了,子類(lèi)和父類(lèi)不是為了解決類(lèi)間具有相同的方法的:
一些動(dòng)物都會(huì)行走,需要擁有行走(Walk)這個(gè)方法,但這不應(yīng)該由子類(lèi)和父類(lèi)實(shí)現(xiàn)。
組合
我們可以這樣解決這個(gè)問(wèn)題:
Class Lion{
walker = new Walk();
walk(){
return walker.walk();
}
}
Class Eagle{
flyer = new Fly();
fly(){
return flyer.fly();
}
}
Class Pegasus{
walker = new Walk();
flyer = new Fly();
walk(){
return walker.walk();
}
fly(){
return flyer.fly();
}
}
組合是簡(jiǎn)單的在新類(lèi)內(nèi)部創(chuàng)建原有類(lèi)對(duì)象。所以組合才是為了解決類(lèi)間具有相同的方法的。在這個(gè)例子里面:
Walk被當(dāng)成“會(huì)行走的動(dòng)物應(yīng)該擁有的方法集合”,同理Fly被當(dāng)成“會(huì)行走的動(dòng)物應(yīng)該擁有的方法集合”,所以對(duì)于天馬(Pegasus),我們只需要對(duì)Walk和Fly進(jìn)行組合就行了。
繼承的目的
繼承并非代碼復(fù)用的唯一方法,但繼承有他的優(yōu)勢(shì):
子類(lèi)可以向上轉(zhuǎn)型變成父類(lèi)。
這樣我們就可以忽略所有的子類(lèi)差異,當(dāng)成相同的類(lèi)來(lái)操作,例如:
我們有方法fn(A),fn(B),這兩個(gè)方法實(shí)際是相似的,我們想復(fù)用他們。
則我們可以通過(guò)設(shè)立一個(gè)父類(lèi)C,其中A是C的子類(lèi),B是C的子類(lèi),那么fn(C)就可以復(fù)用在A和B身上了。
回到Javascript
但回到Javascript,我們發(fā)現(xiàn)上面的例子是不成立的。
因?yàn)镴avascript本身是弱類(lèi)型語(yǔ)言,它并不會(huì)在操作前(因?yàn)樗挥镁幾g)關(guān)注自己操作的對(duì)象類(lèi)型是什么。他只會(huì)執(zhí)行成功,或者發(fā)生錯(cuò)誤。
這時(shí)候,繼承顯得并不必要了。那么類(lèi)也就同樣不是必要的了。
I have been writing JavaScript for 8 years now, and I have never once found need to use an uber function. The super idea is fairly important in the classical pattern, but it appears to be unnecessary in the prototypal and functional patterns. I now see my early attempts to support the classical model in JavaScript as a mistake.
——Douglas Crockford
我寫(xiě)Javascript代碼已經(jīng)8年了,但我從來(lái)沒(méi)有發(fā)現(xiàn)需要使用超類(lèi)函數(shù)。超類(lèi)的想法在古典設(shè)計(jì)模式是非常重要的,但這在以原型和函數(shù)為基調(diào)的模式中并不必要。我現(xiàn)在覺(jué)得,早期我試圖讓Javascript支持經(jīng)典模式是一個(gè)錯(cuò)誤的決定。
安全環(huán)境
當(dāng)然,你可以手動(dòng)去判斷類(lèi)型,控制參數(shù)的類(lèi)型,進(jìn)而提供一個(gè)較為安全的環(huán)境。
例如同樣作為弱類(lèi)型腳本語(yǔ)言的PHP,為了模擬強(qiáng)類(lèi)型面向?qū)ο笳Z(yǔ)言設(shè)置安全環(huán)境,不得不這么做:
class ShopProductWriter{
public function write( $shopProduct ){
if( ! ( $shopProduct instanceof CdProduct ) && ! ( $shopProduct instanceof BookProduct ) ){
die( "輸入錯(cuò)誤的類(lèi)型" );
}
//如果類(lèi)型正確就執(zhí)行一些代碼
}
}
——PHP Objects, Patterns, and Practtice Third Edition . Matt Zandstra
但這只是一個(gè)非常丑陋的方案而已。
經(jīng)典繼承語(yǔ)法糖實(shí)現(xiàn)
不過(guò)經(jīng)典繼承依然是許多人喜歡的方式。所以YUI、Prototype、Dojo、MooTools都提供了自己的實(shí)現(xiàn)方案。
其中較為常見(jiàn)的方案中,語(yǔ)法大概是這樣的:
var Person = Class.extend({
init: function(isDancing){
this.dancing = isDancing;
}
});
var Dancer = Person.extend({
init: function(){
this._super( true );
}
});
var n = new Dancer();
alert(n.dancing); //true
最重要的實(shí)現(xiàn)是對(duì)this._super的實(shí)現(xiàn),其實(shí)extend函數(shù)只是將傳進(jìn)來(lái)的對(duì)象重新組裝了一下,變成一個(gè)原型對(duì)象,新構(gòu)造函數(shù)的prototype里。
具體實(shí)現(xiàn)請(qǐng)查看參考文獻(xiàn)1。
ECMAScript 6的經(jīng)典繼承語(yǔ)法糖
對(duì)于類(lèi)庫(kù)各自實(shí)現(xiàn),導(dǎo)致經(jīng)典繼承語(yǔ)法眾多,ECMA組織貌似不太滿意,他們?cè)噲D在ECMAScript 6中加入更加直觀的經(jīng)典繼承語(yǔ)法糖:
class Animal {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Dog extends Animal {
constructor(name) {
super(name);
}
bark() {
console.log("Woof!");
}
}
總結(jié)
實(shí)際上,Javascript中經(jīng)典繼承并不是必要的。
然而基于許多人喜歡經(jīng)典繼承模型,所以在新版的ECMAScript 6中提供了相關(guān)語(yǔ)法糖。
不過(guò)在中國(guó),前端想廣泛使用該語(yǔ)法糖應(yīng)該是一個(gè)遙遠(yuǎn)的故事……
Quiz2
那Javascript特有的繼承呢?
原型繼承
原型繼承不是解決經(jīng)典繼承中的集合包含關(guān)系的,實(shí)際上原型繼承是解決從屬關(guān)系的,用數(shù)學(xué)表達(dá)就是:
Sub.prototype ∈ Sup
子級(jí)構(gòu)造函數(shù)(子類(lèi)型)原型是一個(gè)父級(jí)構(gòu)造函數(shù)(父類(lèi)型)構(gòu)建的實(shí)例對(duì)象。原型實(shí)際上是子類(lèi)型實(shí)例中需要共享的東西:
function Being(){
this.living = true;
}
Being.prototype.walk = function(){
alert("I' m walking");
};
function Dancer(){
this.dancing = true;
}
Dancer.prototype = new Being();
Dancer.prototype.dance = function(){
alert("I'm dancing");
};
var one = new Dancer();
one.walk();
one.dance();
利用借用、寄生等技術(shù)可以產(chǎn)生很多不同的繼承效果,但這些技術(shù)都只是為了解決原型繼承中屬性和方法一些公用與非公用的問(wèn)題罷了。由于篇幅問(wèn)題就不展開(kāi)討論了,有興趣可以參考《Javascript高級(jí)程序設(shè)計(jì)》相關(guān)內(nèi)容。
思考題
1.文章開(kāi)頭關(guān)于天馬(Pegasus)的題目,如果在Javascript上,應(yīng)該如何設(shè)計(jì)呢?例如我們有下列兩個(gè)類(lèi)型:
function Walk(){
this.walk = function(){
//walk
};
}
function Fly(){
this.fly = function(){
//fly
};
}
Javascript真的需要類(lèi)(Class)么?
我們首先先看下其他有類(lèi)(Class)的面向?qū)ο笳Z(yǔ)言(如:Java)的一些特性。
父類(lèi)與子類(lèi)
父類(lèi)(Superclass)和子類(lèi)(Subclass),并不是為了解決父親與兒子的問(wèn)題,而是為了解決類(lèi)的包含關(guān)系的,我們用Sub表示“子類(lèi)”,用Sup表示“父類(lèi)”,則有:
Sub Sup
這是有區(qū)別的,例如通常我們能夠?qū)⒆宇?lèi)當(dāng)成父類(lèi)來(lái)使用,但認(rèn)人的時(shí)候我們并不能把兒子當(dāng)成父親。
或者可以這么說(shuō),父類(lèi)和子類(lèi)不是為了解決類(lèi)間存在相同方法或者屬性的。
舉個(gè)例子
有人喜歡這樣做:
我們需要一些動(dòng)物的類(lèi),以便在屏幕上創(chuàng)建一些移動(dòng)的動(dòng)物,但移動(dòng)的動(dòng)物有些在空中飛行,有些在路上行走。
所以創(chuàng)建兩個(gè)父類(lèi),一個(gè)是Fly,一個(gè)是Walk:
復(fù)制代碼 代碼如下:
Class Fly{
Fly(){}
}
Class Walk{
Walk(){}
}
然后獅子們(還可以再建些其他的在路上行走的動(dòng)物)就屬于Walk類(lèi),老鷹們(也還可以再建些其他在天上飛行的動(dòng)物)就屬于Fly類(lèi):
復(fù)制代碼 代碼如下:
Class Lion extend Walk{
}
Class Eagle extend Fly{
}
最后對(duì)Lion和Eagle類(lèi)創(chuàng)建一些實(shí)例,調(diào)用相應(yīng)的方法,屏幕上就會(huì)有一些獅子和老鷹在移動(dòng)了。
但這可能并不是一個(gè)好的設(shè)計(jì),比如明天老板突然一拍大腦,他要有一種叫天馬(Pegasus)的動(dòng)物,它們即會(huì)在天上飛,又會(huì)在路上走,時(shí)而要飛行,時(shí)候要行走。
在這種情況下,這個(gè)方案就全然無(wú)用了。
為什么這個(gè)設(shè)計(jì)失敗了?
繼承是有條件的,子類(lèi)必須能?chē)?yán)格的向上轉(zhuǎn)型(變成父類(lèi))。
在上面這個(gè)例子中:
獅子(Lion)被假設(shè)等同于行走動(dòng)物(Walk),老鷹(Eagle)被假設(shè)等同于飛行動(dòng)物(Fly)。
這看起來(lái)很成功,因?yàn)樽宇?lèi)能?chē)?yán)格向上轉(zhuǎn)型,但他有隱患。
當(dāng)有一種天馬(Pegasus)介入到里面的時(shí)候,我們才發(fā)現(xiàn)獅子其實(shí)只是“會(huì)行走的動(dòng)物”,老鷹其實(shí)只是“會(huì)飛行的動(dòng)物”,這不意味著動(dòng)物一輩子只能飛行或者行走,所以即會(huì)飛行又會(huì)行走的天馬就找不到自己的歸屬了。
這個(gè)例子很好的證明了,子類(lèi)和父類(lèi)不是為了解決類(lèi)間具有相同的方法的:
一些動(dòng)物都會(huì)行走,需要擁有行走(Walk)這個(gè)方法,但這不應(yīng)該由子類(lèi)和父類(lèi)實(shí)現(xiàn)。
組合
我們可以這樣解決這個(gè)問(wèn)題:
復(fù)制代碼 代碼如下:
Class Lion{
walker = new Walk();
walk(){
return walker.walk();
}
}
Class Eagle{
flyer = new Fly();
fly(){
return flyer.fly();
}
}
Class Pegasus{
walker = new Walk();
flyer = new Fly();
walk(){
return walker.walk();
}
fly(){
return flyer.fly();
}
}
組合是簡(jiǎn)單的在新類(lèi)內(nèi)部創(chuàng)建原有類(lèi)對(duì)象。所以組合才是為了解決類(lèi)間具有相同的方法的。在這個(gè)例子里面:
Walk被當(dāng)成“會(huì)行走的動(dòng)物應(yīng)該擁有的方法集合”,同理Fly被當(dāng)成“會(huì)行走的動(dòng)物應(yīng)該擁有的方法集合”,所以對(duì)于天馬(Pegasus),我們只需要對(duì)Walk和Fly進(jìn)行組合就行了。
繼承的目的
繼承并非代碼復(fù)用的唯一方法,但繼承有他的優(yōu)勢(shì):
子類(lèi)可以向上轉(zhuǎn)型變成父類(lèi)。
這樣我們就可以忽略所有的子類(lèi)差異,當(dāng)成相同的類(lèi)來(lái)操作,例如:
我們有方法fn(A),fn(B),這兩個(gè)方法實(shí)際是相似的,我們想復(fù)用他們。
則我們可以通過(guò)設(shè)立一個(gè)父類(lèi)C,其中A是C的子類(lèi),B是C的子類(lèi),那么fn(C)就可以復(fù)用在A和B身上了。
回到Javascript
但回到Javascript,我們發(fā)現(xiàn)上面的例子是不成立的。
因?yàn)镴avascript本身是弱類(lèi)型語(yǔ)言,它并不會(huì)在操作前(因?yàn)樗挥镁幾g)關(guān)注自己操作的對(duì)象類(lèi)型是什么。他只會(huì)執(zhí)行成功,或者發(fā)生錯(cuò)誤。
這時(shí)候,繼承顯得并不必要了。那么類(lèi)也就同樣不是必要的了。
I have been writing JavaScript for 8 years now, and I have never once found need to use an uber function. The super idea is fairly important in the classical pattern, but it appears to be unnecessary in the prototypal and functional patterns. I now see my early attempts to support the classical model in JavaScript as a mistake.
——Douglas Crockford
我寫(xiě)Javascript代碼已經(jīng)8年了,但我從來(lái)沒(méi)有發(fā)現(xiàn)需要使用超類(lèi)函數(shù)。超類(lèi)的想法在古典設(shè)計(jì)模式是非常重要的,但這在以原型和函數(shù)為基調(diào)的模式中并不必要。我現(xiàn)在覺(jué)得,早期我試圖讓Javascript支持經(jīng)典模式是一個(gè)錯(cuò)誤的決定。
安全環(huán)境
當(dāng)然,你可以手動(dòng)去判斷類(lèi)型,控制參數(shù)的類(lèi)型,進(jìn)而提供一個(gè)較為安全的環(huán)境。
例如同樣作為弱類(lèi)型腳本語(yǔ)言的PHP,為了模擬強(qiáng)類(lèi)型面向?qū)ο笳Z(yǔ)言設(shè)置安全環(huán)境,不得不這么做:
復(fù)制代碼 代碼如下:
class ShopProductWriter{
public function write( $shopProduct ){
if( ! ( $shopProduct instanceof CdProduct ) && ! ( $shopProduct instanceof BookProduct ) ){
die( "輸入錯(cuò)誤的類(lèi)型" );
}
//如果類(lèi)型正確就執(zhí)行一些代碼
}
}
——PHP Objects, Patterns, and Practtice Third Edition . Matt Zandstra
但這只是一個(gè)非常丑陋的方案而已。
經(jīng)典繼承語(yǔ)法糖實(shí)現(xiàn)
不過(guò)經(jīng)典繼承依然是許多人喜歡的方式。所以YUI、Prototype、Dojo、MooTools都提供了自己的實(shí)現(xiàn)方案。
其中較為常見(jiàn)的方案中,語(yǔ)法大概是這樣的:
復(fù)制代碼 代碼如下:
var Person = Class.extend({
init: function(isDancing){
this.dancing = isDancing;
}
});
var Dancer = Person.extend({
init: function(){
this._super( true );
}
});
var n = new Dancer();
alert(n.dancing); //true
最重要的實(shí)現(xiàn)是對(duì)this._super的實(shí)現(xiàn),其實(shí)extend函數(shù)只是將傳進(jìn)來(lái)的對(duì)象重新組裝了一下,變成一個(gè)原型對(duì)象,新構(gòu)造函數(shù)的prototype里。
具體實(shí)現(xiàn)請(qǐng)查看參考文獻(xiàn)1。
ECMAScript 6的經(jīng)典繼承語(yǔ)法糖
對(duì)于類(lèi)庫(kù)各自實(shí)現(xiàn),導(dǎo)致經(jīng)典繼承語(yǔ)法眾多,ECMA組織貌似不太滿意,他們?cè)噲D在ECMAScript 6中加入更加直觀的經(jīng)典繼承語(yǔ)法糖:
復(fù)制代碼 代碼如下:
class Animal {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Dog extends Animal {
constructor(name) {
super(name);
}
bark() {
console.log("Woof!");
}
}
總結(jié)
實(shí)際上,Javascript中經(jīng)典繼承并不是必要的。
然而基于許多人喜歡經(jīng)典繼承模型,所以在新版的ECMAScript 6中提供了相關(guān)語(yǔ)法糖。
不過(guò)在中國(guó),前端想廣泛使用該語(yǔ)法糖應(yīng)該是一個(gè)遙遠(yuǎn)的故事……
Quiz2
那Javascript特有的繼承呢?
原型繼承
原型繼承不是解決經(jīng)典繼承中的集合包含關(guān)系的,實(shí)際上原型繼承是解決從屬關(guān)系的,用數(shù)學(xué)表達(dá)就是:
Sub.prototype ∈ Sup
子級(jí)構(gòu)造函數(shù)(子類(lèi)型)原型是一個(gè)父級(jí)構(gòu)造函數(shù)(父類(lèi)型)構(gòu)建的實(shí)例對(duì)象。原型實(shí)際上是子類(lèi)型實(shí)例中需要共享的東西:
復(fù)制代碼 代碼如下:
function Being(){
this.living = true;
}
Being.prototype.walk = function(){
alert("I' m walking");
};
function Dancer(){
this.dancing = true;
}
Dancer.prototype = new Being();
Dancer.prototype.dance = function(){
alert("I'm dancing");
};
var one = new Dancer();
one.walk();
one.dance();
利用借用、寄生等技術(shù)可以產(chǎn)生很多不同的繼承效果,但這些技術(shù)都只是為了解決原型繼承中屬性和方法一些公用與非公用的問(wèn)題罷了。由于篇幅問(wèn)題就不展開(kāi)討論了,有興趣可以參考《Javascript高級(jí)程序設(shè)計(jì)》相關(guān)內(nèi)容。
思考題
1.文章開(kāi)頭關(guān)于天馬(Pegasus)的題目,如果在Javascript上,應(yīng)該如何設(shè)計(jì)呢?例如我們有下列兩個(gè)類(lèi)型:
復(fù)制代碼 代碼如下:
function Walk(){
this.walk = function(){
//walk
};
}
function Fly(){
this.fly = function(){
//fly
};
}
您可能感興趣的文章:
- 基于JavaScript實(shí)現(xiàn)繼承機(jī)制之構(gòu)造函數(shù)方法對(duì)象冒充的使用詳解
- 基于JavaScript實(shí)現(xiàn)繼承機(jī)制之調(diào)用call()與apply()的方法詳解
- 基于JavaScript實(shí)現(xiàn)繼承機(jī)制之原型鏈(prototype chaining)的詳解
- Javascript中 關(guān)于prototype屬性實(shí)現(xiàn)繼承的原理圖
- JavaScript對(duì)象創(chuàng)建及繼承原理實(shí)例解剖
- 關(guān)于JavaScript的面向?qū)ο蠛屠^承有利新手學(xué)習(xí)
- Javascript繼承(上)——對(duì)象構(gòu)建介紹
- 淺談javascript的原型繼承
- 關(guān)于JavaScript中原型繼承中的一點(diǎn)思考
- JavaScript面向?qū)ο笾甈rototypes和繼承
- javascript類(lèi)式繼承新的嘗試
- javascript是怎么繼承的介紹
- javascrip關(guān)于繼承的小例子
相關(guān)文章
javascript SpiderMonkey中的函數(shù)序列化如何進(jìn)行
JavaScript中如何進(jìn)行函數(shù)序列化,函數(shù)序列化的作用是什么?本文將介紹SpiderMonkey中的函數(shù)序列化,有需要的朋友可以參考下2012-12-12Javascript入門(mén)學(xué)習(xí)第四篇 js對(duì)象和數(shù)組
上篇文章講了js中的變量,表達(dá)式,和運(yùn)算符 還有一些 js 語(yǔ)句. 這章我們來(lái)探討js中的對(duì)象和數(shù)組。2008-07-07js中的setInterval和setTimeout使用實(shí)例
這篇文章主要介紹了javascript中的兩個(gè)定時(shí)執(zhí)行函數(shù)setInterval和setTimeout的用法,需要的朋友可以參考下2014-05-05js對(duì)象內(nèi)部訪問(wèn)this修飾的成員函數(shù)示例
這篇文章主要介紹了js對(duì)象內(nèi)部訪問(wèn)this修飾的成員函數(shù)示例,需要的朋友可以參考下2014-04-04