如何回答關於 JS 的繼承

Guokai發表於2019-07-25

序言

最近從某個大佬的github部落格上看到一個關於js繼承的部落格,先放上來供大家參考:

JavaScript深入之繼承的多種方式和優缺點

看完之後,總結了幾個點:

為什麼說寄生組合式繼承是最優的?

作者引用了高程的解釋:

引用《JavaScript高階程式設計》中對寄生組合式繼承的誇讚就是:

這種方式的高效率體現它只呼叫了一次 Parent 建構函式,並且因此避免了在 Parent.prototype 上面建立不必要的、多餘的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用 instanceof 和 isPrototypeOf。開發人員普遍認為寄生組合式繼承是引用型別最理想的繼承正規化。

當然,這個肯定是沒有問題,我們在延伸一點東西出來:

這裡為了方便,我把相關程式碼一起貼過來,供參考

涉及原型鏈繼承的問題

程式碼如下:

function Parent () {
    this.names = ['kevin', 'daisy'];
}

function Child () {}

Child.prototype = new Parent();

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy", "yayu"]
複製程式碼

js的物件導向是基於原型和原型鏈的,不像java這種語言,java中的繼承會真正生成一個與父類完全無關的子類,子類new出來的例項是一個單獨的例項,不論你new多少個都是隔離的,然而js並不是這樣的,熟悉js的小夥伴都知道,用原型鏈繼承會導致一個很大的問題,就是“共享父類屬性”的問題,所有的子類例項會共享一個屬性,就好比你有蘋果這個屬性,但是你的蘋果實際上並不是你的,是你從父類那裡繼承過來的,而且,父類可以吃掉你的蘋果,你的兄弟姐妹們也可以吃掉你的蘋果,好吧,想想就可怕。

使用單純的呼叫父類建構函式繼承的問題

程式碼(有改動):

function Parent () {
    this.names = ['kevin', 'daisy'];
    this.getNames = fucntion(){
        return this.names;
    }
}

function Child () {
    Parent.call(this);
}

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.getNames()); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.getNames()); // ["kevin", "daisy"]
複製程式碼

可以看到共享的問題解決了,但是,有一個額外的問題,我們是基於原型鏈的,但是我們並沒有真正的去利用原型鏈的共享功能,完全拋棄了它,並且導致每次new 例項的時候,都會去呼叫父類的構造方法去加到子類的例項上,是完全的copy paste過程,這等於捨棄了js原型鏈的精髓部分,這樣的程式碼自然是沒有靈魂的~

組合繼承?

程式碼:

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {

    Parent.call(this, name);
    
    this.age = age;

}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');

child1.colors.push('black');

console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');

console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]
複製程式碼

new Child的過程中會呼叫一次父類的構造方法,並且在指定Child的原型的時候,又會呼叫一次,這明顯會造成一些問題,child例項上有一個自己的name,同時還有一個父類給的name,這明顯不是最優的方案~

寄生組合

程式碼:

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}

// 關鍵的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();


var child1 = new Child('kevin', '18');

console.log(child1);
複製程式碼

首先要理解什麼是寄生,原本通過直接把父類的例項物件指給子類的原型的方式中,存在一個比較尷尬的問題,子類可以拿到父類以及父類原型的所有屬性,同時組合模式下,子類上擁有的屬性在父類也會有,因此我們用一箇中間類,這個函式是一個空函式,我們就用它來把原型鏈串起來,但是不給他任何屬性,這樣子類的原型通過這個中間的F的例項,就可以直接訪問到原本想要訪問的父類屬性了,同時new F() 的過程避免了重複呼叫原本父類的過程,比起new Parent(), new F()的效能會更好,副作用也更少,核心思想就是:

屬性,比如name,colors 不需要共享的,通過呼叫父類建構函式

方法,比如getName,需要共享的可以直接設定到父類的原型物件上

那麼,你以為,這真的是最優的js繼承方式嗎?

到底有沒有最優的繼承方式?

從“物件”談起

任何一個自然界的物件,大方面都會有2種屬性,一種靜態屬性【屬性】,一種動態屬性【方法或者行為】,比如對於一個自然人,名字,年齡,性別,地址等等,都是靜態屬性,它們是不可以被共享的,還有動態屬性,比如:說話,走路,吃東西,等等叫做動態屬性,那麼我們在設計繼承的時候,到底應該怎麼劃分?每個人的靜態屬性隔離,動態屬性共享,就夠了嗎?

比如:每一個人都有鼻子,有眼睛,有頭有腦,這些靜態屬性應該隔離嗎?

再比如:羽毛球運動員都會打羽毛球,乒乓球運動員都會打乒乓球,這些動態屬性都應該共享嗎?

當然不是。

最優解

繼承的最優解其實是要看當前應用場景的,最符合預期的場景就是,需要共享的,無論是靜態的還是動態的,把它們放在parent的原型上,需要隔離的,把它們放在parent上,然後子類通過呼叫parent的構造方法來初始化為自身的屬性,這樣,才是真正的“最佳繼承設計方式”。

總結:

當面試者問你的時候,我覺得沒必要答什麼寄生組合什麼的【它只是個名字】,最優的繼承方式:

對於當前需要被設計為共享屬性的屬性,全部通過設定原型的方式掛到父類的原型上,不分靜態和動態,維度劃分是是否可以共享,對於每個子類都不一樣的屬性,應該放到父類的構造方法上,然後通過子類呼叫父類構造方法的方式來實現初始化過程,對於子類獨有的屬性,我們通過擴充套件子類構造方法的方式實現,那麼對於每一個子類如何拿到父類的原型方法,就需要將子類的構造方法的原型與父類構造方法的原型進行原型鏈關聯的操作,看個具體的例子:

const Man = function(name, age) {
    // 需要都要但是值不一樣的屬性,在父類的構造方法中定義
    this.name = name;
    this.age = age;
};

Man.prototype.say = function() {
    // 需要共享的動態屬性
    console.log(`i am ${this.name}, i am ${this.age} years old`);
};

Man.prototype.isMan = true; // 需要共享的靜態屬性

const Swimmer = function(name, age, vitalCapacity) {
    Man.call(this, name, age); //繼承“人類”
    this.vitalCapacity = vitalCapacity; // 對於游泳員來說肺活量是很重要的一個指標,是每個游泳員都需要但是值都不同的屬性
};

const BasketBaller = function(name, age, height) {
    Man.call(this, name, age); //繼承“人類”
    this.height = height; // 對於籃球運動員來說身高是一個都需要但是值都不同的指標
};

// 我們用es新的直接設定原型關係的方法來關聯原型鏈
Object.setPrototypeOf(Swimmer.prototype, Man.prototype); // 設定子類原型和父類原型的原型鏈關係 達到共享原型上的屬性的目的
Object.setPrototypeOf(BasketBaller.prototype, Man.prototype); // 同理

// 還可以繼續擴充套件 Swimmer.prototype 或者 BasketBaller.prototype 上的公共屬性哦

const swimmer1 = new Swimmer('swimmer1', 11, 100);
const swimmer2 = new Swimmer('swimmer2', 12, 200);

swimmer1.isMan // true 共享靜態屬性
swimmer1 // age: 11, name: "swimmer1", vitalCapacity: 100 自身屬性有了
swimmer1.say() // i am swimmer1, i am 11 years old 共享動態屬性

const basketBaller1 = new BasketBaller('basketBaller1', 20, 180);
const basketBaller2 = new BasketBaller('basketBaller2', 30, 187);

// 等等等。。。
複製程式碼

所以,不要回答那麼多,但是一定要自己理解了這個過程,只要你知道最好的繼承方式是什麼,相信至少在這個問題上不會卡殼,並且這也是我們日常開發中,可以真正用起來的最佳實踐方案~

相關文章