前言
繼承,作爲複用代碼的一種有效手段,在面向對象編程中有着重要意義。但是這門腳本語言的確不像某些靜態語言那樣提供了真正意義上的基於類實現的繼承方式,而是採用了一種基於原型的繼承。這裏將說說在ES5時,使用JavaScript來實現繼承的幾種方式。
在具體講這些方式之前,先預先說清楚幾個概念。
函數:在JavaScript中,通常每一個函數上都會有一個prototype對象,假如我們通過new這個操作符來使用函數,這時函數的定位更像一個構造器,相當於類那樣,提供了一個模板,而此時的prototype對象就是來描述這個模板的。
原型鏈:當我們嘗試去訪問一個實例的屬性或者方法時。假如當前實例沒有,那麼會委託到他的原型對象上。假如還是沒有,會一直順着原型鏈向上找,直到Object.prototype爲止。現在主流的瀏覽器都提供了__proto__來查找他的原型對象。或者也可以使用Object.getPrototypeOf來獲取。
1). 原型鏈繼承。
// 基於原型鏈繼承
function Parent(name){
this.name = name;
}
Parent.prototype.getName = function(){
return this.name;
}
Parent.prototype.foo = ["parent"];
function Child(){
}
Child.prototype = new Parent();
const parent = new Parent('parent')
const child = new Child('child');
parent.foo.push('hello');
console.log(parent.foo); // [ 'parent', 'hello' ]
console.log(child.foo); // [ 'parent', 'hello' ]
console.log(child.name); // undefined
console.log(parent instanceof Parent); //true
console.log(child instanceof Parent); //true
console.log(child instanceof Child); //true
這段代碼可以說明幾個問題。
- 假如原型對象上存在引用屬性,任何一個實例只要改變了這個引用指向數據結構的內容,另外一個實例在訪問時也會受到影響,當然大多數時候這不是我們需要的,我們只是希望每一個實例也有同樣的屬性而已。
- 當我們實例化Child時,打印出的name是undefined,當然原因是Child這個構造器原本就沒有使用到name屬性,但我們希望的是,能夠複用到Parent這個構造器,而不是在子類構造器中再寫一遍。
- 此時,child 實例已經屬於 Parent這個“父類”,因爲他們之間通過原型鏈“串”了起來。最後的instanceof 判斷相當於以下代碼
console.log(parent.__proto__ == Parent.prototype);
console.log(child.__proto__.__proto__ == Parent.prototype);
console.log(child.__proto__ == Child.prototype);
2) 在子類的構造器中調用父類構造器的方法。
// 借用父類構造器函數
function Parent(name,age){
this.name = name;
this.age = age;
this.role = 'parent';
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(){
const args = Array.prototype.slice.call(arguments)
Parent.apply(this,args)
this.role = 'child';
}
const parent = new Parent('p',50);
const child = new Child('c',20);
console.log(parent.name); // p
console.log(parent.age); //50
console.log(parent.getName()); // 50
console.log(child.name); //c
console.log(child.age); //20
console.log(child.getName && child.getName()); // undefined
console.log(parent instanceof Parent); //true
console.log(child instanceof Parent); // false
console.log(child instanceof Child); // true
- 首先現在每一個實例都有獨立的屬性,並且Child的構造方法內調用了父類構造器,能夠複用到部分代碼
- 在子類的實例想要使用getName這個方法時,實際上是找不到的。因爲此時這個方法在Parent這個構造器的原型上。當然,也可以把這個方法的聲明放到構造器上,或者在Child這個原型上再申請一次,不過這樣就又一次陷入了代碼冗餘的怪圈中。
- 此時child instanceof Parent 返回false,我們依舊希望他返回true
3) 將原型鏈繼承和借用父類構造器結合起來,組合式繼承
// 借用父類構造器函數
function Parent(name,age){
this.name = name;
this.age = age;
this.role = 'parent';
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(){
const args = Array.prototype.slice.call(arguments)
Parent.apply(this,args)
this.role = 'child';
}
Child.prototype = new Parent();
const parent = new Parent('p',50);
const child = new Child('c',20);
console.log(parent.name); //p
console.log(parent.age); //50
console.log(parent.getName()); //p
console.log(parent.role); //parent
console.log(child.name); //c
console.log(child.age); //20
console.log(child.getName && child.getName()); //c
console.log(child.role); //child
console.log(parent instanceof Parent); true
console.log(child instanceof Parent); // true
console.log(child instanceof Child); // true
- 此時,我們發現這樣的繼承方式已經解決了上述提到的很多問題,有那麼點意思了
- 但是仔細看就會發現,我們在實現這種繼承時,父類的構造器被調用了2次。如果父類的構造器函數十分複雜,那這樣的操作也不是我們想要的。
4)原型式繼承
// 原型式繼承
const foo = {
name:'xxx',
age:28,
getName(){
return this.name;
}
};
const clone = Object.create(foo);
console.log(clone.name); // xxx
console.log(clone.age); // 28
console.log(clone.getName()); // xxx
這個要區分開和原型鏈繼承的區別。
- 可以看到,之前的繼承都是首先要有一個父類的構造器,然後子類再去想方設法繼承他,實現代碼的複用。而在這裏,我們通過Object.create這個API,也能實現代碼複用。這個API的實現相當於以下的clone函數。
function clone(obj){
var F = function(){};
F.prototype= obj;
return new F();
}
- 這裏與其說是“繼承”,不如說是克隆來的貼切。我們直接返回了一個匿名構造器的實例,這個實例的原型對象指向了一個目標對象,這樣一來,當我們去訪問這個實例的某個屬性時,就會去委託我們指定的這個對象。
- 從clone函數實現來看,假如我們傳入的obj中有引用類型的屬性,多個實例將會共享他,也會有互相影響的問題。
5)寄生繼承
原型式繼承本質上還是利用了對象的淺複製來實現代碼的複用,但是我們的目標對象假如存在引用屬性,克隆後的對象都能訪問到這一屬性,假如我們希望每一個克隆的對象希望有自己的屬性可以這麼做。
// 寄生式繼承
const foo = {
name:'xxx',
age:28,
getName(){
return this.name;
}
};
function cloneDecorator(obj){
const clone = Object.create(obj);
clone.selfAttributes = ['left','right'];
clone.getSelfAttrs = function(){
return this.selfAttributes;
}
return clone;
}
const obj1 = cloneDecorator(foo);
const obj2 = cloneDecorator(foo);
obj1.selfAttributes.push(1);
obj2.selfAttributes.push(2);
console.log(obj1.getName()); // xxx
console.log(obj1.getSelfAttrs()); // [ 'left', 'right', 1 ]
console.log(obj2.getName()); // xxx
console.log(obj2.getSelfAttrs()); // [ 'left', 'right', 2 ]
- cloneDecorator這個方法實際上有點裝飾器模式的味道,我們除了返回這個匿名構造器的實例之外,還對他額外做了一些屬性增強,當然,這裏又回到了之前碰到的問題,getSelfAttrs這個方法在每一個實例中都聲明瞭一次。但如果放到foo這個對象中,又會產生多個實例互相影響的問題。
- 不過原型式繼承和寄生式繼承提供了一種思路,我們想要把原型鏈串起來,不一定非得去調用父類的構造器,我們可以直接淺複製父類構造器的原型。這就引出了下面一種繼承的實現方式。
6)寄生組合式繼承。
將上面提到的3和5結合起來,我們可以寫出以下代碼
// 借用父類構造器函數
function Parent(name,age){
this.name = name;
this.age = age;
this.role = 'parent';
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(){
const args = Array.prototype.slice.call(arguments)
Parent.apply(this,args)
this.role = 'child';
}
function inherit(subType,superType) {
const subTypePrototype = Object.create(superType.prototype);
subTypePrototype.constructor = subType;
subType.prototype = subTypePrototype;
}
inherit(Child,Parent)
const parent = new Parent('p',50);
const child = new Child('c',20);
console.log(parent.name);
console.log(parent.age);
console.log(parent.getName());
console.log(parent.role);
console.log(child.name);
console.log(child.age);
console.log(child.getName && child.getName());
console.log(child.role);
console.log(parent instanceof Parent);
console.log(child instanceof Parent);
console.log(child instanceof Child);
可以看看打印結果和組合式繼承是一樣的,inherit函數替代了原來的new操作。這樣的繼承方式相對來說整合了各種繼承的優點。
談談class的繼承
在ES6中,提供了class這樣的語法糖,注意只是語法糖,JavaScript是基於原型來實現繼承的。我們可以在babel上來看看,一個class的extends做了什麼。
可見一斑不是麼。