JAVAScript 繼承全解析(建議收藏)
在大多數(shù)的編程語言中,繼承都是一個重要的主題。
在那些基于類的編程語言中,繼承提供了兩個有用的服務。首先,它是代碼重用的一種形式。如果一個新的類和一個已經(jīng)存在的類大部分相似,那么你只須說明其不同點即可。
代碼重用的模式極為重要,因為它們很有可能顯著地減少軟件開發(fā)的成本。類繼承的另一個好處是它包括了一套類型系統(tǒng)的規(guī)范。由于程序員無需顯式類型轉(zhuǎn)換的代碼,他們的工作量將大大的減輕,這是一件很好的事情,因為類型轉(zhuǎn)換時會丟失類型系統(tǒng)在安全上的好處。
JavaScript 是一門弱類型語言,從不需要類型轉(zhuǎn)換。對象的起源是無關緊要的,對于一個對象來說重要的是它能做什么,而不是它從哪里來。
JavaScript 提供了一套更為豐富的代碼重用模式,它可以模擬那些基于類的模式,同時它也可以支持其他更具表現(xiàn)力的模式。在 JavaScript 可能的繼承模式有很多。
在基于類的語言中,對象是類的實例,并且類可以從另一個類繼承而來。JavaScript 是一門基于原型的語言,這意味著對象直接從其他對象繼承。
原型可以直接從對象繼承。
偽類
JavaScript 的原型存在著諸多的矛盾。某些看起來有點像基于類的語言的復雜語法問題遮蔽了它的原型機制。它不讓對象直接從其他對象繼承,反而插入了一個多余的間接層,從而使構(gòu)造器函數(shù)產(chǎn)生對象。
當一個函數(shù)對象被創(chuàng)建時, Function 構(gòu)造器產(chǎn)生的函數(shù)對象會運行類似這樣一個代碼:
this.prototype = {
concstructor: this
}
新函數(shù)對象被賦予一個 prototype 屬性,其值是包含一個 constructor 屬性且屬性值為該函數(shù)對象。該 prototype 對象是存放繼承特征的地方。因為 JavaScript 語言沒有提供一種方法去確定哪個函數(shù)是打算來做構(gòu)造器的,所以每個函數(shù)都會得到一個 prototype 對象。constructor 屬性沒什么用。重要的是 prototype 對象。
當采用構(gòu)造器調(diào)用模式,即使用 new 前綴去調(diào)用一個函數(shù)時,這將修改函數(shù)執(zhí)行的方式。
如果 new 運算符是一個方法,而不是一個運算符,它可能會像這樣執(zhí)行。
if(typeof Object.beget !== 'function') {
Object.beget = function(o) {
var F = function() {}
F.prototype = o
return new F()
}
}
Function.prototype.method = function (name, func) {
if(!this.prototype[name]) {
this.prototype[name] = func
return this
}
}
Function.method('new', function () {
// 創(chuàng)建一個新對象,它繼承自構(gòu)造器函數(shù)的原型對象.
var that = Object.beget(this.prototype)
// 調(diào)用構(gòu)造器函數(shù),綁定 this 到新新對象。
var other = this.Apply(that, arguments)
// 如果它的返回值不是一個對象,就返回該對象。
return (typeof other === 'object' && other) || that
})
我們可以定義一個構(gòu)造器并擴充它的原型
var Mammal = function(name) {
this.name = name
}
Mammal.prototype.get_name = function () {
return this.name
}
Mammal.prototype.says = function () {
return this.saying || ''
}
現(xiàn)在,我們可以構(gòu)造一個實例
var myMammal = new Mammal('jeason')
var name = myMammal.get_name()
我們可以構(gòu)造另一個偽類來繼承 Mammal,這是通過定義它的 constructor 函數(shù)并替換它的 prototype 為一個 Mammal 的實例來實現(xiàn)的。
var Cat = function(name) {
this.name = name;
this.saying = 'meow';
}
// 替換 Cat.prototype 為一個新的 Mammal 實例
Cat.prototype = new Mammal()
Cat.prototype.purr = function(n) {
return 'purr'
}
Cat.prototype.get_name = function() {
return this.says() + ' ' + this.name + ' ' + this.says()
}
var myCat = new Cat('foobar')
console.log(myCat.says())
console.log(myCat.purr(5))
console.log(myCat.get_name())
console.log(myCat)
偽類模式本意是想向面向?qū)ο罂繑n,但它看起來格格不入。我們可以隱藏一些丑陋的細節(jié),這是通過使用 method 方法定義一個 inherits 方法來實現(xiàn)的
Function.method('inherits', function (Parent) {
this.prototype = new Parent()
return this
})
我們的 inherits 和 method 方法都返回 this,這將允許我們可以以級聯(lián)的樣式編程。
var BigCat = function(name) {
this.name = name;
this.saying = 'meow';
}
BigCat.inherits(Mammal).
method('purr', function (n) {
return 'purr'
}).
method('get_name', function () {
return this.says() + ' ' + this.name + ' ' + this.says()
});
console.log(BigCat)
var myCat1 = new BigCat('foobar')
console.log(myCat1.says())
console.log(myCat1.purr(5))
console.log(myCat1.get_name())
console.log(myCat1)
通過隱藏那些「無謂的」 prototype 操作細節(jié),現(xiàn)在它看起來沒那么怪異了。但我們是否真的有所改善呢?我們現(xiàn)在有了行為像「類」的構(gòu)造器函數(shù),但仔細去看,它們可能有著令人驚訝的行為:沒有私有環(huán)境,所有屬性都是公開的。無法訪問父類的方法。
更糟糕的是,使用構(gòu)造器函數(shù)存在一個嚴重的危害。如果你在調(diào)用構(gòu)造器函數(shù)時忘記在前面加上 new 前綴,那么 this 將不會被綁定到一個新對象上??杀氖?, this 將被綁定到全局對象上,所以你不但沒有擴充新對象,反而破壞了全局變量。這真的是糟糕透頂,發(fā)生了那樣的情況時,既沒有編譯時警告,也沒有運行時警告。
這是一個嚴重的語言設計錯誤。為了降低這個問題帶來的風險,所有的構(gòu)造器函數(shù)都約定命名成首字母大寫的形式,并且不以首字母大寫的形式拼寫任何其他的東西。這樣我們至少可以通過眼睛檢查是否缺少了 new 前綴。一個更好的備選方案就是根本不使用 new。
「偽類」可以給不熟悉 JavaScript 的程序員提供便利,但它也隱藏了該語言的真實本質(zhì)。借鑒類的表示法可能誤導程序員編寫過于深入與復雜的層次結(jié)構(gòu)。許多復雜的類層次結(jié)構(gòu)產(chǎn)生的原因就是靜態(tài)類型檢查的約束。JavaScript 完全擺脫了那些約束,在基于類的語言中,類的繼承是代碼重用的唯一方式,JavaScript 有著更多且更好的選擇。
對象說明符
有時候,構(gòu)造器要接受一大串參數(shù),這可能是令人煩惱的,因為要記住參數(shù)的順序可能非常困難。在這種情況下,如果我們編寫構(gòu)造器時介紹一個簡單的對象說明符可能更加友好。那個對象包含了將要構(gòu)建的對象規(guī)格說明,所以,與其這么寫:
var obj1 = marker(arg1, arg2, args3)
不如這么寫:
var obje2 = marker({
first: arg1,
setcond: arg2,
third: arg3
})
現(xiàn)在多個參數(shù)可以按照任意順序排列,如果構(gòu)造器聰明地使用默認值,一些參數(shù)可以忽略掉,并且代碼也更容易閱讀。
當與 JSON 一起工作時,這還可以有一個間接的好處。JSON 文本只能描述數(shù)據(jù),但有時數(shù)據(jù)表示的一個對象,將該數(shù)據(jù)與它的方法關聯(lián)起來是有用的。如果構(gòu)造器取得一個對象說明符,可以容易做到,因為我們可以簡單傳遞該 JSON 對象構(gòu)造器,而它將返回一個構(gòu)造完全的對象。
原型
在一個純粹的原型模式中,我們會摒棄類,轉(zhuǎn)而專注于對象。基于原型的繼承相比基于類的繼承的概念上更為簡單:一個新對象可以繼承一個舊對象的屬性。也許你對此感到陌生,但它真的很容易理解。你通過構(gòu)造一個有用的對象開始,接著可以構(gòu)造更多和那個對象類似的對象??梢酝耆苊獍岩粋€應用拆解成一系列嵌套抽象類的分類過程。
讓我們先用對象字面量去構(gòu)造一個有用的對象。
var myMammal = {
name: 'foobar',
get_name: function() {
return this.name
},
says: function () {
return this.saying || ''
}
}
一旦有了一個對象,我們就可以利用 Object.beget 方法構(gòu)造出更多的實例出來,接下來我們要定制新的實例:
var myMammal = {
name: 'foobar',
get_name: function() {
return this.name
},
says: function () {
return this.saying || ''
}
}
var myCat2 = Object.beget(myMammal)
myCat2.name = 'foo bar Jeason'
mycat2.saying = 'meow'
mycat2.purr = function() {
return 'purr'
}
mycat2.get_name = function () {
return this.syas + ' ' + this.name + ' ' + this.says
}
這是一種「差異化繼承」,通過定制一個新的對象,我們指明了它與所基于的基本對象的區(qū)別。
有時候,它對某種數(shù)據(jù)結(jié)構(gòu)從其他數(shù)據(jù)結(jié)構(gòu)繼承的情形非常有用。這里就有一個例子:假定我們要解析一門類似 JavaScript 那樣一對用花括號指示作用域的語言。定義在一個作用域中的條目在該作用域之外是不可見的。從某種意義來理解,也就是說一個內(nèi)部作用域會繼承它的外部作用域。JavaScript 在表示這樣的關系上做得非常好。當遇到一個花括號時 block 函數(shù)將從 scope 中尋找符號,并且當它定義了新的符號時擴充 scope:
var block = function () {
// 記住當前的作用域,構(gòu)造了一個包含了當前作用域中所有的對象的新作用域
var oldScope = scope;
scope = Object.beget(scope)
// 傳遞花括號作為參數(shù)調(diào)用 advance
advance("{")
// 使用新的作用域進行解析
parse(scope)
// 傳遞右花括號作為參數(shù)調(diào)用 advance 并拋棄新作用域,恢復原來老的作用域
advance("}")
scope = oldScope
}
函數(shù)化
迄今為止,所看到的繼承模式的一個弱點就是我們沒法保護隱私。對象的所有屬性都是可見的。我們沒法得到私有變量和私有函數(shù)。有時候那不要緊,但有時候卻是大麻煩。遇到這些麻煩的時候,一些不知情的程序員接受了一種偽裝私有的模式。如果想構(gòu)造一個私有的屬性,他們就給其起一個怪模怪樣的名字,并且希望其他使用代碼的用戶假裝看不到這些奇怪的成員元素。幸運的是,我們有一個更好的選擇,那就是模塊模式的應用。
我們從構(gòu)造一個將產(chǎn)生對象的函數(shù)開始,給它起的名字將以一個小寫字母開頭,因為它并不需要使用 new 前綴。該函數(shù)包括四個步驟。
1.它創(chuàng)建了一個新對象。有很多的方式去構(gòu)造一個對象。它可以構(gòu)造一個對象字面量,或者它可以和 new 前綴連用去調(diào)用一個構(gòu)造器函數(shù),或者它可以使用 Object.beget 方法去構(gòu)造一個已經(jīng)存在的新對象的實例,或者它可以調(diào)用任意一個會返回一個對象的函數(shù)。2.它選擇性地定義私有實例變量和方法。這些就是函數(shù)中通過 var 語句定義的普通變量。3.它給這個新對象擴充方法。那些方法將擁有特權(quán)去訪問參數(shù),以及在第二步中通過 var 語句定義的變量。4.它返回那個新對象。
這里是一個函數(shù)構(gòu)造器的偽代碼版本
var constructor = function(spec, my) {
var foo, bar
my = my || {}
// 把共享的變量和函數(shù)添加到 my 中
that = new object
// 添加給 that 的特權(quán)方法
that.test = function() {}
return that
}
spec 對象包含構(gòu)造器需要構(gòu)造一個新實例的所有信息。spec 的內(nèi)容可能會被復制到私有變量中,或者被其他函數(shù)改變?;蛘叻椒梢栽谛枰臅r候訪問 spec 的信息。
my 對象是一個繼承中的構(gòu)造器提供秘密共享的容器。my 對象可以選擇性地使用,如果沒有傳入一個 my 對象,那么會創(chuàng)建一個 my 對象。
接下來,聲明該對象私有的實例變量和方法。通過簡單的聲明變量就可以做到。構(gòu)造器變量和內(nèi)部函數(shù)變成了該實例的私有成員。內(nèi)部函數(shù)可以訪問 spec、my、that,以及其他私有變量。
接下來,給 my 對象添加共享的秘密成員。這是通過賦值語句來做的:
my.member = value
現(xiàn)在,我們構(gòu)造了一個新對象并將其賦值給 that。有很多方式可以構(gòu)造一個新對象,我們可以使用對象字面量,可以用 new 運算符調(diào)用一個偽類構(gòu)造器,傳給它一個 spec 對象和 my 對象。my 對象允許其他的構(gòu)造器分享我們放到 my 中的資料。其他的構(gòu)造器可能也會將自己的可分享的秘密成員放進 my 對象中,以便我們的構(gòu)造器可以使用它。
接下來,我們擴充 that,加入組成該對象接口的特權(quán)方法,我們可以分配一個新函數(shù)成為 that 的成員方法,或者,更安全的,我們可以先將函數(shù)定義為私有方法,然后再將它們分配給 that:
var methdical = function () {}
that.methdiccal = methdical
分兩步去定義 methdical 的好處是,如果其他方法想要調(diào)用 methdical,它們可以直接調(diào)用 methdical(),而不是 that.methdical()。如果該實例被破壞或篡改,甚至 that.methdical 被替換掉了,調(diào)用 methdical 的方法將同樣會繼續(xù)工作,因為它們私有的 methdical 不受該實例的影響。
最后,我們返回 that
讓我們這個模式應用到 mammal 例子里,此處不需要 my,所以我們先拋開它,但將使用一個 spec 對象。
name 和 saying 屬性現(xiàn)在是完全私有的,它們只有通過 get_name 和 says 兩個特權(quán)方法才可以訪問。
var mammal = function(spec) {
var that = {}
that.get_name = function() {
return spec.name
}
that.says = function() {
return spec.saying || ''
}
return that
}
var myMammal = mammal({
name: 'foo bar'
})
在偽類模式中,構(gòu)造器函數(shù) Cat 不得不重復構(gòu)造器 Mammal 已經(jīng)完成的工作。在函數(shù)化模式中那不再需要了,因為構(gòu)造器 Cat 將會調(diào)用構(gòu)造器 Mammal,讓 Mammal 去做對象創(chuàng)建中的大部分工作,所以 Cat 只需要關注自身的差異即可。
var cat = function(spec) {
spec.saying = spec.saying || 'meow'
var that = mammal(spec)
that.purr = function () {
return 'purr'
}
that.get_name = function() {
return that.says() + ' ' + spec.name + ' ' + that.says()
}
return that
}
var myCat = cat({name: 'jeaosn'})
console.log(myCat)
函數(shù)化模式還給我們提供了一個處理父類方法的方法,我們將構(gòu)造一個 superior 方法,它取得一個方法名并返回調(diào)用那個方法的函數(shù)。該函數(shù)將調(diào)用原來的那個方法,盡管屬性已經(jīng)變化了。
Object.method('superior', function(name) {
var that = this
var method = that[name]
return function() {
return method.apply(that,arguments)
}
})
讓我們在 coolcat 上試驗一下, coolcat 就像是 cat 一樣,除了它有一個更酷的調(diào)用父類方法的 get_name 方法。它只需要一點點準備工作,我們將聲明一個 super_get_name 變量,并且把調(diào)用 superior 方法所返回的結(jié)果賦值給它。
var coolcat = function(spec) {
var that = cat(spec)
var super_get_name = that.superior('get_name');
that.get_name = function() {
return 'like' + ' ' + super_get_name() + 'baby'
}
return that
}
var myCoolCat = coolcat({name: 'josh'})
var name= myCoolCat.get_name()
console.log(name)
console.log(myCoolCat)
函數(shù)化模式有很大的靈活性,它不僅不像偽類模式那樣需要很多功夫,還讓我們得到更好的封裝和信息隱藏,以及訪問父類方法的能力。
如果對象的所有狀態(tài)都是私有的,那么該對象就成為一個「防偽」對象。該對象的屬性可以被替換或刪除,但該對象的完整性不會受到損害。如果我們用函數(shù)化的樣式創(chuàng)建一個對象,并且該對象的所有方法都不使用 this 或 that,那么該對象就是持久性的。一個持久性對象就是一個簡單功能函數(shù)的集合。
一個持久性對象不會被損害,訪問一個持久性對象時,除非被方法授權(quán),否則攻擊者不能訪問對象的內(nèi)部狀態(tài)。
部件
我們可以從一套部件中組合出對象來。例如,我們可以構(gòu)造一個添加簡單事件處理特性到任何對象上的函數(shù)。它會給對象添加一個 on 方法,一個 fire 方法和一個私有的事件注冊對象。
var eventuality = function(that) {
var registry = {}
that.fire = function () {
// 在一個對象是觸發(fā)一個事件,該事件可以是一個包含事件名稱的字符串
// 或是擁有一個包含事件名稱 type 屬性的對象
// 通過 ‘on' 方法注冊的事件處理程序中匹配事件名稱的函數(shù)將被調(diào)用
var array
var func
var handle
var i
var type = typeof event === 'string' ? event : event.type
// 如果這個事件存在一組事件處理程序,那么就遍歷它們并順序依次執(zhí)行
if (registry.hasOwnProperty(type)) {
array = registry[type];
for(i = 0; i< array.lenth;i += 1) {
handle = array[i]
}
// 每組處理程序包含一個方法和一個可選的參數(shù)
// 如果該方法是一個字符串形式的名字,那么尋找到該函數(shù)
func =handle.method
if (typeof func === 'string') {
func =this[func]
}
// 調(diào)用一個處理程序,如果該條目包含參數(shù),那么傳遞它們進去。否則,傳遞該事件對象
func.apply(this, handle.parameters || [event]);
}
return this
}
that.on = function(type, method, parameters) {
// 注冊一個事件,構(gòu)造一條處理程序條目,將它插入到處理程序數(shù)組中
// 如果這種類型還不存在,那就構(gòu)造一個
var handler = {
method: method,
parameters: parameters
}
if(registry.hasOwnProperty(type)) {
registry[type].push(handler)
} else {
registry[type] = [handler]
}
return this
}
return that
}
我們可以在任何單獨的對象上調(diào)用 eventuality,授予它事件處理方法。我們也可以趕在 that 被返回之前在一個構(gòu)造器函數(shù)中調(diào)用它。
eventuality(that)
用這種方式,一個構(gòu)造器函數(shù)可以從一套部件中組裝出對象。JavaScript 的弱類型在此處是一個巨大的優(yōu)勢,因為我們無需花費精力去關注一個類型系統(tǒng)的類族譜。相反,我們專注它的個性內(nèi)容。