原型鏈繼承
原型鏈繼承是比較常見的繼承方式之一,其中涉及的構(gòu)造函數(shù)、原型和實例,三者之間存在著一定的關(guān)系,即每一個構(gòu)造函數(shù)都有一個原型對象,原型對象又包含一個指向構(gòu)造函數(shù)的指針,而實例則包含一個原型對象的指針。例如:
function Parent1() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child1() {
this.type = 'child2';
}
Child1.prototype = new Parent1();
console.log(new Child1());
上面的代碼其實有一個潛在的問題,例如:
var s1 = new Child1();
var s2 = new Child1();
s1.play.push(4);
console.log(s1.play);
console.log(s2.play);
執(zhí)行結(jié)果如下:
當我修改了s1的play屬性的時候,s2的play屬性也跟著變了,因為兩個實例使用的是同一個原型對象。它們的內(nèi)存空間是共享的,當一個發(fā)生變化的時候,另外一個也隨之進行了變化,這就是使用原型鏈繼承方式的一個缺點。
構(gòu)造函數(shù)繼承(借助 call)
function Parent1(){
this.name = 'parent1';
}
Parent1.prototype.getName = function () {
return this.name;
}
function Child1(){
Parent1.call(this);
this.type = 'child1'
}
let child = new Child1();
console.log(child); // 沒問題
console.log(child.getName()); // 會報錯
運行結(jié)果如下:
除了 Child1 的屬性 type 之外,也繼承了 Parent1 的屬性 name。這樣寫的時候子類雖然能夠拿到父類的屬性值,解決了第一種繼承方式的弊端,但問題是,父類原型對象中一旦存在父類之前自己定義的方法,那么子類將無法繼承這些方法。
因此構(gòu)造函數(shù)實現(xiàn)繼承的優(yōu)缺點,它使父類的引用屬性不會被共享,優(yōu)化了第一種繼承方式的弊端;但是隨之而來的缺點也比較明顯——只能繼承父類的實例屬性和方法,不能繼承原型屬性或者方法。
組合繼承(前兩種組合)
這種方式結(jié)合了前兩種繼承方式的優(yōu)缺點,結(jié)合起來的繼承,代碼如下:
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次調(diào)用 Parent3()
Parent3.call(this);
this.type = 'child3';
}
// 第一次調(diào)用 Parent3()
Child3.prototype = new Parent3();
// 手動掛上構(gòu)造器,指向自己的構(gòu)造函數(shù)
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play); // 不互相影響
console.log(s4.play);
console.log(s3.getName()); // 正常輸出'parent3'
console.log(s4.getName()); // 正常輸出'parent3'
結(jié)果如下:
之前方法一和方法二的問題都得以解決,但是這里又增加了一個新問題:通過注釋我們可以看到 Parent3 執(zhí)行了兩次,第一次是改變Child3 的 prototype 的時候,第二次是通過 call 方法調(diào)用 Parent3 的時候,那么 Parent3 多構(gòu)造一次就多進行了一次性能開銷。
原型式繼承
ES5 里面的 Object.create 方法,這個方法接收兩個參數(shù):一是用作新對象原型的對象、二是為新對象定義額外屬性的對象(可選參數(shù))。
let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name);
console.log(person4.name === person4.getName());
console.log(person5.name);
console.log(person4.friends);
console.log(person5.friends);
執(zhí)行結(jié)果如下:
通過 Object.create 這個方法可以實現(xiàn)普通對象的繼承,不僅僅能繼承屬性,同樣也可以繼承 getName 的方法。前三個輸出都是正常的,最后兩個輸出結(jié)果一致是因為Object.create 方法是可以為一些對象實現(xiàn)淺拷貝的,那么關(guān)于這種繼承方式的缺點也很明顯,多個實例的引用類型屬性指向相同的內(nèi)存。
寄生式繼承
使用原型式繼承可以獲得一份目標對象的淺拷貝,然后利用這個淺拷貝的能力再進行增強,添加一些方法,這樣的繼承方式就叫作寄生式繼承。
雖然其優(yōu)缺點和原型式繼承一樣,但是對于普通對象的繼承方式來說,寄生式繼承相比于原型式繼承,還是在父類基礎(chǔ)上添加了更多的方法。實現(xiàn)如下:
let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName());
console.log(person5.getFriends());
輸出結(jié)果如下:
從最后的輸出結(jié)果中可以看到,person5 通過 clone 的方法,增加了 getFriends 的方法,從而使 person5 這個普通對象在繼承過程中又增加了一個方法,這樣的繼承方式就是寄生式繼承。
寄生組合式繼承
結(jié)合第四種中提及的繼承方式,解決普通對象的繼承問題的 Object.create 方法,我們在前面這幾種繼承方式的優(yōu)缺點基礎(chǔ)上進行改造,得出了寄生組合式的繼承方式,這也是所有繼承方式里面相對最優(yōu)的繼承方式,代碼如下:
function clone (parent, child) {
// 這里改用 Object.create 就可以減少組合繼承中多進行一次構(gòu)造的過程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child5';
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person6 = new Child6();
console.log(person6);
console.log(person6.getName());
console.log(person6.getFriends());
-----------------------------------
執(zhí)行結(jié)果如下:
這種寄生組合式繼承方式,基本可以解決前幾種繼承方式的缺點,較好地實現(xiàn)了繼承想要的結(jié)果,同時也減少了構(gòu)造次數(shù),減少了性能的開銷。整體看下來,這六種繼承方式中,寄生組合式繼承是這六種里面最優(yōu)的繼承方式。
ES6的extends關(guān)鍵字實現(xiàn)邏輯
ES6提供了extends語法糖,使用關(guān)鍵字很容易實現(xiàn)JAVAScript的繼承,先看一下extends使用方法。
class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以簡寫為 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子類中存在構(gòu)造函數(shù),則需要在使用“this”之前首先調(diào)用 super()。
super(name)
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功訪問到父類的方法
使用babel將ES6 的代碼編譯成 ES5,代碼如下:
function _possibleConstructorReturn (self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}
function _inherits (subClass, superClass) {
// 這里可以看到
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Parent = function Parent () {
// 驗證是否是 Parent 構(gòu)造出來的 this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child () {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).Apply(this, arguments));
}
return Child;
}(Parent));