物件導向和基於物件
幾乎每個開發人員都有物件導向語言(比如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);//動物
複製程式碼