繼承的那些事

繼承是 OO 語言中一個最爲津津樂道的概念,許多 OO 語言都支持兩種繼承方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。因爲函數沒有簽名,在 ECMAScript 中沒法實現接口繼承。ECMAScript 只支持實現繼承並且實現繼承主要是依靠原型鏈來實現的。

關於原型鏈,我以前的文章裏面有介紹,若是有些忘記了,能夠看這篇文章
下面我將詳細的介紹前端前輩在開發過程當中不斷摸索創造的幾種繼承方式。看完面試的時候千萬不要簡單的回答 call 跟 apply 了。
爲了提及來省事,雖然 js 沒有嚴格意義的類,我仍是以父類和子類來作區分繼承關係。javascript

1. prototype模式繼承

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

// 定義父類
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 的所有方法,可是也是有缺點的。java

  1. 建立子類實例時,沒法向父類構造函數傳參。指定 prototype 時,實例化 Person 傳的參數,會出如今全部子類上,不靈活。
  2. 由圖能夠看到,p 的 contructor 指向 Person, 因此 Child.prototype.constructor 也指向 Person,顯然會致使繼承鏈的紊亂。

2.借用構造函數繼承

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

// 定義父類
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);

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

  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,顯然也是不合理的。app

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面向對象編程(二):構造函數的繼承
相關文章
相關標籤/搜索