繼承那些事

leizore發表於2018-06-25

繼承是 OO 語言中一個最為津津樂道的概念,許多 OO 語言都支援兩種繼承方式:介面繼承和實現繼承。介面繼承只繼承方法簽名,而實現繼承則繼承實際的方法。由於函式沒有簽名,在 ECMAScript 中無法實現介面繼承。ECMAScript 只支援實現繼承而且實現繼承主要是依靠原型鏈來實現的。

關於原型鏈,我之前的文章裡面有介紹,如果有些忘記了,可以看這篇文章。 下面我將詳細的介紹前端前輩在開發過程中不斷摸索創造的幾種繼承方式。看完面試的時候千萬不要簡單的回答 call 跟 apply 了。 為了說起來省事,雖然 js 沒有嚴格意義的類,我還是以父類和子類來做區分繼承關係。

1. prototype模式繼承

既然子類想要繼承父類的全部方法,而且我們知道父類的例項擁有父類所有的方法,那麼接下類就好辦了,我將子類的 prototype 指向父類的例項,子類就擁有了父類的全部方法了

// 定義父類
function Parent (name, age) {
    this.name = name;
    this.age = age;
}
Parent.prototype.sayName = function () {
    alert(this.name);
}
// 定義子類
function Child (sex) {
    this.sex = sex;
}
// 實現繼承
var p = new Parent('leizore', 25);
Child.prototype = p;
var child = new Child('男');
child.sayName();            // leizore
複製程式碼

那麼對應的關係圖如下:

chain1.png

這種方式 Child 繼承了 Person 的全部方法,但是也是有缺點的。

  1. 建立子類例項時,無法向父類建構函式傳參。指定 prototype 時,例項化 Person 傳的引數,會出現在所有子類上,不靈活。
  2. 由圖可以看到,p 的 contructor 指向 Person, 所以 Child.prototype.constructor 也指向 Person,顯然會導致繼承鏈的紊亂。

2.借用建構函式繼承

針對上面的繼承方法的缺點1,開發人員使用一種叫做借用建構函式的技術,也就是我們平時說的 call 跟 apply 繼承。

// 定義父類
function Parent (name, age) {
    this.name = name;
    this.age = age;
}
Parent.prototype.sayName = function () {
    alert(this.name);
}
// 定義子類
function Child (name, age, sex) {
     // 繼承,同時傳遞了引數
    Parent.call(this, name, age)
    this.sex = sex;
}

複製程式碼

這裡簡單講一下 call(apply)是如何實現的,其實就是將 call(apply) 前面的函式立即執行一遍,並且執行時將作用域 this 指向 call(apply) 函式的第一個引數,比如這裡的 call 就是將 Parent 例項一遍,將 name 跟 age 當成引數傳過去 這種繼承方式解決了繼承過程中的傳參問題,但是缺點是並沒有繼承到父類的原型,為了解決這個問題,我們很容易想到將上面兩個方法結合起來不久好了。於是另一種繼承方式出現了

3.組合繼承

沒錯,就是兩種方式並用,從而發揮兩者之長的一種繼承模式,程式碼如下

// 定義父類
function Parent (name, age) {
    this.name = name;
    this.age = age;
}
Parent.prototype.sayName = function () {
    alert(this.name);
}
// 定義子類
function Child (name, age, sex) {
    // 繼承,同時傳遞了引數
    Parent.call(this, name, age)
    this.sex = sex;
}
Child.prototype = new Parent('leizore', 25);
複製程式碼

嗯,這種方式基本上解決了開發過程中繼承的痛點,成為好多人常用的繼承模式之一。但是缺點也是有的

  1. 重複定義了屬性,可以看到將 Child 的 prototype指向 Perent 的例項時,繼承了name 跟 age 屬性,例項 Child 的時候,呼叫 call 函式,又繼承了一次,雖然使用 call 呼叫這次的屬性是在例項屬性上,當獲取name時優先返回例項屬性,然後在 prototype 上,所以並不會出大問題。
  2. 第一種繼承方式方式的缺點二也完美的繼承過來了,Child.prototype.constructor 還是指向 parent 那麼肯定有人會說,既然Child.prototype.constructor 不指向自己,那麼直接讓他指向自己不就好了?
Child.prototype.constructor = Child;
複製程式碼

答案是不行的。因為 Child.prototype 是 Parent 的例項,這樣操作會將 Parent.prototype.constructor 也指向 Child,顯然也是不合理的。

4.原型式繼承###

為了解決上面 Child 與 Parent 繼承之後糾纏不清的問題,道格拉斯在2006年提出一種繼承方法,它的想法是藉助原型可以給予已有的物件建立新物件,同時還不必因此建立自定義型別。函式如下

function object (o) {
    function F() {}
    F.prototype = o;
    return new F();
}
複製程式碼

這個模式相當與建立一個新的物件,物件繼承了o所有屬性,當然這裡也只是實現了淺拷貝。

5.組合寄生式繼承

嗯,想必大家也想到了,上面這種繼承方式可以解決 Child 與 Parent 繼承後的糾纏不清的關係。可以由 object 方法建立一個臨時物件,從而斬斷跟 Parent 的聯絡。就可以放心的對 Child 原型的constructor 隨便指了,當然了為了繼承鏈的不紊亂,還是指向自己比較好

// 定義父類
function Parent (name, age) {
    this.name = name;
    this.age = age;
}
Parent.prototype.sayName = function () {
    alert(this.name);
}
// 定義子類
function Child (name, age, sex) {
    // 繼承,同時傳遞了引數
    Parent.call(this, name, age)
    this.sex = sex;
}
function object (o) {
    function F() {}
    F.prototype = o;
    return new F();
}
var prototype = object(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;


var c = new Child('leizore', 11, 'men');
c.sayName()                // leizore
c.constructor === Child    // true
複製程式碼

到此,基本上解決了上面所說的所有缺點。當然了,也是有一點問題的,就是方法四的實現其實是淺拷貝,如果 Parent.prototype 裡又引用型別比如陣列,物件,改變Parent.prototype,Child 也會跟著變,解決方式也很簡單,使用深拷貝就行了,同時又可以寫很多繼承方式。當然了,按照我上面順下來的思想,也可以寫出自己的繼承方式 比如下面改變object函式:

// 定義父類
function Parent (name, age) {
    this.name = name;
    this.age = age;
}
Parent.prototype.sayName = function () {
    alert(this.name);
}
// 定義子類
function Child (name, age, sex) {
    // 繼承,同時傳遞了引數
    Parent.call(this, name, age)
    this.sex = sex;
}
function object (o) {
    var c = {};
   for (var i in o) {
     c[i] = o[i];
   }
   return c
}
var prototype = object(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;


var c = new Child('leizore', 11, 'men');
c.sayName()                // leizore
c.constructor === Child    // true
複製程式碼

當然了,es6 中,可以通過extends關鍵字實現繼承,這裡就不多說了

參考

  1. javascript 高階程式設計
  2. Javascript物件導向程式設計(二):建構函式的繼承

相關文章