JavaScript中的函式繼承
幾乎每個開發人員都有面向物件語言(比如C++、C#、Java)的開發經驗。在傳統面向物件的語言中,有兩個非常重要的概念——類和例項。類定義了一些事物公共的行為和方法;而例項則是類的一個具體實現。我們還知道,面向物件程式設計有三個重要的概念——封裝、繼承和多型。 但是在Javascript的世界中,所有的這一切特性似乎都不存在。因為Javascript本身不是面向物件的語言,而是基於物件的語言。Javascript中所有事物都是物件,包括字串、陣列、日期,甚至是函式,請看一個有趣的例項:
//定義一個函式 function add(a,b){ add.invokeTimes++; return a+b; } //因為函式本身也是物件,在這裡我們為add定義一個屬性,用來記錄次函式被呼叫的次數 add.invokeTimes = 0; add(1,1); add(2,2); console.log(add.invokeTimes);//2 複製程式碼
模擬Javascript中類和繼承
在面向物件的語言中,我們使用類來建立一個自定義物件。然而Javascript中所有事物都是物件,那麼用什麼方法來建立自定義物件呢? 在這裡我們引入一個新概念——原型(prototype),我們可以簡單的把prototype看做是一個模板,新建立的自定義物件都是這個模板(prototye)的一個拷貝(實際上不是拷貝而是連結,只不過這種連結是不可見,給人的感覺好像是拷貝)。 使用prototype建立自定義物件的一個例子:
//建構函式 function Person(name,gender){ this.name = name; this.gender = gender; } //定義Person的原型,原型中的屬性可以被自定義物件引用 Person.prototype = { getName:function(){ return this.name; }, getGender:function() { return this.gender; } } 複製程式碼
這裡我們把函式Person稱為建構函式,也就是建立自定義物件的函式。可以看出,Javascript通過結構函式和原型的方式模擬實現了類的功能。 建立自定義物件(例項化類):
var Person1 = new Person("張三","男"); console.log(Person1.getName());//張三 var Person2 = new Person("娜娜","女"); console.log(Person2.getName());//娜娜 複製程式碼
當代碼var Person1 = new Person("張三","男")執行時,其實內部做了如下幾件事情:
建立一個空白物件(new Object())。 拷貝Person.prototype中的屬性(鍵值對)到這個空物件中(我們前面提到,內部實現時不是拷貝而是一個隱藏的連結)。 將這個物件通過this關鍵字傳遞到建構函式中並執行建構函式。 將這個物件賦值給變數Person1。
為了證明prototype模板並不是被拷貝到例項化的物件中,而是一種連結的方式,請看如下例項:
function Person(name,gender){ this.name = name; this.gender= gender; } Person.prototype.age = 20; var Person1 = new Person('娜娜','女'); console.log(Person1.age); //覆蓋prototype中的age屬性 Person1.age = 25; console.log(Person1.age);//25 delete Person1.age; //在刪除例項屬性age後,此屬性值又從prototype中獲取 console.log(Person1.age);//20 複製程式碼
Javascript繼承的幾種方式
為了闡述Javascript繼承的幾種方式,首先我們提前約定共同語言:
//約定 function Fun(){ //私有屬性 var val = 1;//私有基本屬性 var arr = [1];//私有引用屬性 function fun() {}//私有函式(引用屬性) //例項屬性 this.val = 1;//公有基本屬性 this.arr = [1];//公有引用屬性 this.fun = function(){};//公有函式(引用屬性) } //原型屬性 Fun.prototype.val = 1;//原型基本屬性 Fun.prototype.arr = [1];//原型引用屬性 Fun.prototype.fun = function(){};//原型函式(引用屬性) 複製程式碼
一、簡單原型鏈實現繼承
這是實現繼承最簡單的方式了。 如果“貓”的prototype物件,指向一個Animal的示例,那麼所有“貓”的例項,就能繼承Animal了。
具體實現
function Animal(){ this.species = "動物"; this.classes = ['脊椎動物','爬行動物']; } function Cat(name,color){ this.name = name; this.color = color; } //將Cat的prototype物件指向一個Animal的例項 Cat.prototype = new Animal(); Cat.prototype.constructor = Cat; var cat1 = new Cat("大毛","黃色"); var cat2 = new Cat("二毛","白色"); cat1.classes.push('哺乳動物'); cat1.species = '哺乳動物'; console.log(cat1.species);//哺乳動物 console.log(cat2.species);//動物 console.log(cat1.classes);//["脊椎動物", "爬行動物", "哺乳動物"] console.log(cat2.classes);//["脊椎動物", "爬行動物", "哺乳動物"] 複製程式碼
我們將Cat的prototype物件指向一個Animal的示例。
Cat.prototype = new Animal(); 複製程式碼
它相當於完全刪除了prototype物件原先的值,然後賦予一個新值。
Cat.prototype.constructor = Cat; 複製程式碼
任何一個prototype物件都有一個constructor屬性,指向它的建構函式。如果沒有“Cat.prototype = new Animal(); ”這一行,Cat.prototype.constructor是指向Cat的;加了這一行以後,Cat.prototype.constructor指向Animal。
console.log(Cat.prototype.constructor == Animal);//true 複製程式碼
更重要的是,每一個例項也有一個constructor屬性,預設呼叫prototype物件的constructor屬性。
console.log(cat1.constructor = Cat.prototype.constructor);//true 複製程式碼
因此,在執行“Cat.prototype = new Animal();”這一行之後,cat1.constructor也指向了Animal!
console.log(cat1.constructor == Animal);//true 複製程式碼
這顯然會導致繼承鏈的紊亂(cat1明明是建構函式Cat生成的),因此我們必須手動糾正,將Cat.prototype物件的constructor值改為Cat。 這一點很重要,程式設計時務必遵守。如果替換裡prototype物件 ,
o.prototype = {}; 複製程式碼
那麼,下一步必然是為新的prototype物件加上contructor屬性,並將這個屬性指回原來的建構函式。
o.prototype.constructor = o; 複製程式碼
存在的問題
1.修改cat1.classes後cat2.classes也發生了變化,因為來自原型物件的引用屬性是所有例項共享的。可以這樣理解:執行cat1.classes.push('哺乳動物');先對cat1進行屬性查詢,找遍了例項屬性(在本例中沒有例項屬性),沒找到,就開始順著原型鏈向上找,拿到了cat1的原型物件,一查詢,發現有classes屬性。於是給classes末尾插入了‘哺乳動物’,所喲cat2.classes也發生了變化。
2.建立子類例項時,無法向父類建構函式傳遞引數。
二、借用建構函式和call或者apply方法
簡單原型鏈真夠簡單,可是存在兩個致命的缺點簡直無法使用,於是上世紀末的Jsers就想辦法修復了這兩個缺陷,然後就出現了借用建構函式這種方式。
具體實現
function Animal(species){ this.species = species; this.classes = ['脊椎動物','爬行動物']; } function Cat(name,color,species){ Animal.call(this,species);//核心 this.name = name; this.color = color; } var cat1 = new Cat("大毛","黃色",'動物'); var cat2 = new Cat("二毛","白色",'哺乳動物'); cat1.classes.push('哺乳動物'); console.log(cat1.species);//動物 console.log(cat2.species);//哺乳動物 console.log(cat1.classes);//["脊椎動物", "爬行動物", "哺乳動物"] console.log(cat2.classes);//["脊椎動物", "爬行動物"] 複製程式碼
核心
借父類的建構函式來增強子類例項,等於是把父類的例項屬性複製了一份給子類例項裝上了(完全沒有用到原型)。
優缺點
優點:1.解決了子類例項共享父類引用屬性的問題; 2.建立子類例項時,可以向父類建構函式傳參。缺點: 無法實現函式複用,每個子類例項都持有一個新的fun函式,太多了就會影響效能,記憶體爆炸。
三、組合繼承(最常用)
目前我們借用建構函式方式還是有問題(無法實現函式複用),沒關係,接著修復,於是出現了組合繼承。
具體實現
function Animal(species){ //只在此處聲明基本屬性和引用屬性 this.species = species; this.classes = ['脊椎動物','爬行動物']; } //在此處宣告函式 Animal.prototype.eat = function(){ console.log('動物必須吃東西獲取能量'); } Animal.prototype.run = function(){ console.log('動物正在跑動'); } function Cat(name,color,species){ Animal.call(this,species);//核心 this.name = name; this.color = color; } Cat.prototype = new Animal(); var cat1 = new Cat("大毛","黃色",'動物'); var cat2 = new Cat("二毛","白色",'哺乳動物'); cat1.classes.push('哺乳動物'); console.log(cat1.species);//動物 console.log(cat2.species);//哺乳動物 console.log(cat1.classes);//["脊椎動物", "爬行動物", "哺乳動物"] console.log(cat2.classes);//["脊椎動物", "爬行動物"] console.log(cat1.eat === cat2.eat);//true 複製程式碼
具體實現
把例項函式都放在原型物件上,以實現函式複用。同時還要保留借用建構函式方式的優點,通過Animal.call(this,species)繼承父類的基本屬性和引用屬性並保留能傳參的優點;通過Cat.prototype = new Animal(),繼承父類函式,實現函式複用。
優缺點
優點:1.不存在引用屬性共享的問題 2.可傳參 3.函式可以複用缺點: (一點小瑕疵)子類原型上有一份多餘的父類例項屬性,因為父類建構函式被呼叫了兩次,生成了兩份,而子類例項上的那一份遮蔽了子類原型上的。又是記憶體浪費,不過已經改進了很多。
四、直接繼承prototype(改進簡單原型鏈繼承)
第四種方法是對第二種方法的改進。由於Animal物件中,不變的屬性都可以直接寫入Animal.prototype。所以,我們也可以讓Cat()跳過Animal(),直接繼承Animal.prototype。
具體實現
function Animal(){} Animal.prototype.species = '動物'; function Cat(name,color){ this.name = name; this.color = color; } //將Cat的prototype物件指向Animal的prototype物件,這樣就實現了繼承 Cat.prototype = Animal.prototype; Cat.prototype.constructor = Cat; var cat1 = new Cat('大毛','黃色'); console.log(cat1.species);//動物 複製程式碼
優缺點
優點:與第一種方法相比,這樣做的優點是效率比較高(不用執行和建立Animal的示例了),比較省記憶體。缺點: Cat.prototype和Animal.prototype現在指向了同一個物件,那麼任何對Cat.prototype的修改,都會反映到Animal.prototype。 Cat.prototype.constructor = Cat,把Animal.prototype物件的constructor屬性也改掉了
console.log(Animal.prototype.constructor);//Cat 複製程式碼
五、利用空物件作為中介(寄生組合繼承)
由於“直接繼承prototype”存在上述的缺點,所以就有了以下方法,利用一個空物件作為中介。
function Animal(){} Animal.prototype.species = '動物'; function Cat(name,color){ Animal.call(this); this.name = name; this.color = color; } //利用空物件作為中介,核心 var F = function(){}; F.prototype = Animal.prototype; Cat.prototype = new F(); Cat.prototype.constructor = Cat; 複製程式碼
F是空物件,所以幾乎不佔記憶體。這時,修改Cat的prototype物件,就不會影響到Animal的prototype物件。
console.log(Animal.prototype.constructor);//Animal 複製程式碼
將上述方法封裝成一個函式,便於使用
function extend(Child,Parent){ var F = function(){}; F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; Child.uber = Parent.prototype; } 複製程式碼
使用方法如下:
function Animal(){} Animal.prototype.species = '動物'; function Cat(name,color){ this.name = name; this.color = color; } extend(Cat,Animal); var cat1 = new Cat('大毛','黃色'); console.log(cat1.species);//動物 複製程式碼
函式的最後一行
Child.uber = Parent.prototype; 複製程式碼
為子物件設一個uber屬性,這個屬性直接指向父物件的prototype屬性。(uber是一個德語詞,意思是"向上"、"上一層"。)這等於在子物件上開啟一條通道,可以直接呼叫父物件的方法。這一行放在這裡,只是為了實現繼承的完備性,純屬備用性質。
六、拷貝繼承
上面是採用prototype物件,實現繼承。我們也可以換一種思路,純粹採用“拷貝”方法實現繼承。簡單說,就是把父物件的所有屬性和方法,拷貝進子物件。 定義一個函式,實現屬性拷貝的目的:
function extend(Child,Parent){ var p = Parent.prototype; var c = Child.prototype; for(var i in p){ c[i] = p[i]; } c.uber = p; } 複製程式碼
這個函式的作用就是將父物件的prototype物件中的屬性,一一拷貝給Child物件的prototype物件。 繼承的具體實現如下:
function Animal(){} Animal.prototype.species = '動物'; function Cat(name,color){ this.name = name; this.color = color; } extend(Cat,Animall); var cat = new Cat('大毛','黃色'); console.log(cat.species);//動物 複製程式碼