原型/原型鏈
JS是一門基于原型實現(xiàn)繼承的語言。那么,什么是原型?基于原型實現(xiàn)的繼承又是怎么一回事?
原型(prototype),根據(jù)字面意思,可以理解為一件事物的模板。比如iPhone的原型是以前只能打電話、發(fā)短信的功能機,這表示,iPhone也擁有打電話、發(fā)短信的功能(繼承),但是相比它的原型又擁有了更多功能(可以擴展更多功能)。這種關(guān)系有點類似于Java中的子類與父類的關(guān)系(Java是基于類的繼承,而Javascript是基于原型)。
在JS中,每一個函數(shù)(Function)都有一個prototype屬性,這個prototype對象有一個屬性constructor指向這個構(gòu)造函數(shù):
function Person() {
?
}
Person.prototype.constructor === Person // true
另外,每個JS對象還有一個隱藏屬性proto,指向它的構(gòu)造函數(shù)的原型對象(prototype),即:
var person = new Person();
person._proto_ === Person.prototype; // true
對象實例、構(gòu)造函數(shù)和原型的關(guān)系可以表示成下圖:

更進一步的,我們知道Person.prototype也是一個對象,那么它也擁有proto屬性,指向Person.prototype構(gòu)造函數(shù)的原型對象,這個原型對象又有proto屬性…...通過這樣一個實例對象的proto指向構(gòu)造函數(shù)prototype、prototype對象又擁有proto屬性的指向循環(huán),我們就可以建立起一條原型鏈。
person._proto_ => Person.prototype
Person.prototype._proto_ => Object.prototype
Object.prototype._proto_ === null // true
注意,所有原型鏈的終點是Object.prototype.proto,這個對象沒有對應(yīng)構(gòu)造函數(shù)的原型了,所以為null。
基于原型對象(prototype)和原型鏈,我們就可以實現(xiàn)繼承。
繼承
按照《Javascript高級程序設(shè)計》中所寫,在JS中實現(xiàn)繼承大致有6種方式:
一、借用構(gòu)造函數(shù)
子類型要如何擁有父類型的屬性呢?如果父類的屬性都是通過構(gòu)造函數(shù)定義的,那么最簡單粗暴的方法當(dāng)然是直接在子類的構(gòu)造函數(shù)中調(diào)用父類的構(gòu)造函數(shù),此時子類就具有了父類的所有屬性。
function SuperType(name) {
this.name = name;
this.colors = ['pink', 'blue', 'green'];
}
function SubType(name) {
SuperType.call(this, name);
}
let instance1 = new SubType('Yvette');
instance1.colors.push('yellow');
console.log(instance1.colors);//['pink', 'blue', 'green', yellow]
let instance2 = new SubType('Jack');
console.log(instance2.colors); //['pink', 'blue', 'green']
此時我們已經(jīng)讓SubType擁有了SuperType的屬性。
問題來了,如果我們想讓SubType能夠直接復(fù)用/繼承SuperType的方法,這種繼承的方式就無法實現(xiàn)了。因此這種方式是有缺陷的,在實踐中也不可能單純用它實現(xiàn)繼承。
此時就需要我們前面鋪墊了很久的原型對象出場了。
二、原型鏈繼承
在JS中讀取一個實例屬性時,首先會在該實例上讀取,如果讀取不到需要的屬性,則會在實例的原型上搜索。那么如果我們讓A類型的原型指向另一個B類型,在A類型上讀取不到的屬性就可以接著去A類型的原型(也就是B類型)去讀取,更進一步的會去搜過B類型的原型,一直到原型鏈的末端。這就是原型鏈繼承。
因此我們想要SubType繼承SuperType,只需要讓前者的prototype指向后者的實例就行了。
function SuperType() {
this.name = 'Yvette';
this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.getName = function () {
return this.name;
}
function SubType() {
this.age = 22;
}
SubType.prototype = new SuperType();
SubType.prototype.getAge = function() {
return this.age;
}
SubType.prototype.constructor = SubType;
let instance1 = new SubType();
instance1.colors.push('yellow');
console.log(instance1.getName()); //'Yvette'
console.log(instance1.colors);//[ 'pink', 'blue', 'green', 'yellow' ]
let instance2 = new SubType();
console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ] ,注意這里instance2的屬性和instanse1共享了
此時我們不僅可以繼承父類的屬性,函數(shù)也獲得了繼承。
但是這樣實現(xiàn)繼承的缺陷在于——所有實例的屬性都共享了。這顯然也是不可接受的,我們想要每個實例能有自己的屬性,只用繼承同樣的函數(shù)就行了。此時我們會想到:借用構(gòu)造函數(shù)的繼承可以使每個實例擁有自己的屬性,而原型鏈繼承可以繼承父類的函數(shù),能不能把二者的優(yōu)點集合起來呢?
三、組合繼承(借用構(gòu)造函數(shù) + 原型鏈繼承)
我們可以使用構(gòu)造函數(shù)來實現(xiàn)對屬性的繼承,再用原型鏈實現(xiàn)對方法的繼承,綜合二者各自的優(yōu)點就能實現(xiàn)一個功能完善的繼承了。
function SuperType(name) {
this.name = name;
this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SuberType(name, age) {
SuperType.call(this, name);
this.age = age;
}
SuberType.prototype = new SuperType();
SuberType.prototype.constructor = SuberType;
SuberType.prototype.sayAge = function () {
console.log(this.age);
}
let instance1 = new SuberType('Yvette', 20);
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); //Yvette
let instance2 = new SuberType('Jack', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName();//Jack
如上所示,每個子類的實例都擁有了自己的屬性,并且都繼承了父類的sayName方法(注意父類的方法是定義在prototype對象上的)。
這個方案已經(jīng)基本能在實踐中使用了,但是它還是有一個小問題——父類的構(gòu)造函數(shù)調(diào)用了2次。一次是在子類構(gòu)造函數(shù)中調(diào)用父類構(gòu)造函數(shù),另一次是在把子類的原型用父類的一個實例賦值時。理論上還是有優(yōu)化的空間。
現(xiàn)在我們再來看看另一種繼承的實現(xiàn),也是一種很有名的實現(xiàn)。
四、原型式繼承
借助原型可以基于已有的對象創(chuàng)建新對象,不必因此創(chuàng)建新類型。我們來看一個函數(shù):
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
在object()函數(shù)內(nèi)部創(chuàng)建了一個臨時類型F,然后將傳入的對象作為它的原型并返回了一個實例。本質(zhì)上相當(dāng)于對傳入的對象進行了一次淺拷貝。實踐中我們不需要手寫這個函數(shù),而是可以直接使用Object.create()來實現(xiàn)同樣的功能。
在沒有必要創(chuàng)建單獨的構(gòu)造函數(shù)來實現(xiàn)一些定制功能,只是需要讓兩個對象的行為保持一致時,我們可以使用這樣的原型式繼承。
當(dāng)然它也具有原型鏈繼承的缺點,無法為每個實例創(chuàng)建自己獨有的屬性。
五、寄生式繼承
寄生式繼承是基于原型式繼承的,只不過在創(chuàng)建對象的過程中以某種方式對它進行了一些增強。
function createAnother(original) {
var clone = object(original);// 通過調(diào)用函數(shù)創(chuàng)建一個新對象
clone.sayHi = function () {// 以某種方式增強這個對象
console.log('hi');
};
return clone;// 返回這個對象
}
var person = {
name: 'Yvette',
hobbies: ['reading', 'photography']
};
var person2 = createAnother(person);
person2.sayHi(); //hi
但是依然沒有解決原型式繼承的問題。
根據(jù)前面提到的組合繼承的思路,我們再一次思考能否使用組合多種方案來解決問題。
六、寄生組合式繼承
通過名字我們大概能猜到,這種方式是組合了寄生式繼承、借用構(gòu)造函數(shù)的方式。
首先我們通過寄生式繼承實現(xiàn)方法的繼承:
function inherit(superType, subType) {
var o = object(superType.prototype);
o.constructor = subType; // 注意這一步——維持constructor是subType,因為上一行將prototype設(shè)為了superType
subType.prototype = o;
}
接著我們補充構(gòu)造函數(shù)來實現(xiàn)對實例屬性的繼承:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'yellow'];
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType(name, age) {
SuperType.call(this, name); //只調(diào)用了一次構(gòu)造函數(shù)
this.age = age;
}
inherit(SuperType, SubType);
......
現(xiàn)在我們實現(xiàn)了一個較為完善的繼承:
它既能實現(xiàn)方法的復(fù)用,又能保證每個實例擁有各自的屬性,同時它只調(diào)用了一次構(gòu)造函數(shù),因為我們沒有像原型鏈繼承一樣創(chuàng)建額外的一個父類型的實例給子類型的原型。
這也就是我們實踐中可以使用的一種繼承的實現(xiàn)方式。
ES 6的繼承
ES 6中新增了class和extends關(guān)鍵字,可以讓我們在JS中實現(xiàn)其他基于類的繼承的語言的繼承寫法。
class SubType extends SuperType {
}
當(dāng)然雖然我們可以用如此簡潔的寫法完成繼承,實際上底層實現(xiàn)仍然是基于原型實現(xiàn)的,只不過Babel幫我們完成了這部分轉(zhuǎn)譯工作。而轉(zhuǎn)譯出的代碼實質(zhì)上和我們上面所寫的寄生組合式繼承的代碼是大同小異的。