老掉牙,但永不過期的面向對象——繼承

幾乎全部語言都有面向對象的概念,JavaScript 的面向對象實質是基於原型的對象系統。說到面向對象,不得不提的就是繼承。數組

認識 new

一個沒有其餘語言經驗的人要更容易理解 JavaScript 的繼承。
不一樣於其餘語言的類的繼承,JavaScript 中使用的是原型繼承,不過從表面上看更像是基於類的繼承,緣由多是由於 new 關鍵字的使用。new 關鍵字是用來調用構造函數的,一個函數之因此稱爲構造函數,並非由於函數自己有什麼特性,而是由於 new 。也就是說只有經過 new 調用的函數纔可能成爲構造函數。
new 既然這麼神奇,有必要探究下內部到底實現了什麼。app

  • 建立一個空對象,並讓這個對象繼承構造函數的prototype。
  • 將構造函數的this指向這個空對象,並執行構造函數。
  • 如構造函數執行後返回的是對象類型就直接返回,不然返回上面建立的對象。

下面是簡易的實現代碼:函數

function myNew(constructor, param) {
  // constructor 就是構造函數,param 模擬構造函數的參數,這裏只用一個參數舉例
  
  // 建立一個空對象,且這個空對象繼承構造函數的 prototype 屬性
  const obj = Object.create(constructor.prototype)

  // 將構造函數的 this 指向 obj,執行構造函數獲得返回結果
  const result = constructor.apply(obj, param)

  // 若是造函數執行後,返回結果是對象類型,就直接返回,不然返回 obj 對象
  return (typeof result === 'object' && result != null) ? result : obj
}

function Animal(species) {
  this.species = species
}

const cat = myNew(Animal, '貓科動物')

console.log(cat)

// {species: "貓科動物"}

結合上面 new 實現的三步,再看代碼就比較容易理解了。
須要注意的就是,若是構造函數有顯式返回值,而且返回值類型爲對象。那麼構造函數返回的結果再也不是目標實例,而是這個顯式的返回值。this

如何實現繼承

JavaScript 實現繼承的方式有多種,各有優缺點,下面就將常見的幾種方式一一列出。prototype

1、構造函數的繼承方式code

function Animal(species) {
    this.species = species || "動物"
}
function Cat(name, color, species) {
    Animal.call(this, species)
    this.name = name
    this.color = color
}
Animal.prototype.age = 10
var cat1 = new Cat("毛毛", "黑色", "貓科動物")
console.log(cat1.species) // 貓科動物
console.log(cat1.age) // undefined

這種繼承的實現方式是在子類構造函數中執行父類構造函數,並將父類構造函數的this指向子類的實例。優勢簡單易懂,可是缺點也很明顯。
==缺點==:沒法繼承父類原型上的屬性和方法。orm

2、原型鏈繼承模式對象

function Animal() {
    this.species = "動物"
    this.list = [1, 2, 3]
}
function Cat(name, color) {
    this.name = name
    this.color = color
}

// 將Cat的prototype對象指向一個Animal的實例
// 它至關於徹底覆蓋了 prototype 對象原先的值。
Cat.prototype = new Animal()
// 本行下面會有詳細解釋
Cat.prototype.constructor = Cat

var cat1 = new Cat("大黃", "黃色")
var cat2 = new Cat("小黃", "黑色")

//這種方式實現的繼承,不一樣實例的原型對象都指向同一個Animal的實例,訪問屬性的時候,若是實例內沒有該屬性,就會向上找到Cat.prototype(Animal的一個實例)中。
//可是這裏調用cat1.species去賦值,不會向上尋找,而只是在cat1實例中添加一個species屬性,並不會影響cat1原型對象(Animal實例)中的屬性,所以cat2.species的值沒有變化,這種方式並不能說明問題,看下面的代碼
cat1.species = "貓科動物"
console.log(cat1.species) // 貓科動物
console.log(cat2.species) // 動物

// 調用數組的push方法,就會順着原型鏈搜索,找到原型對象中的list並修改值,上面說了不一樣實例的原型對象都指向同一個Animal的實例,因此 cat2.list 讀取到的值也是改變後的。
cat1.list.push(4)
console.log(cat1.list) // [1,2,3,4]
console.log(cat2.list) // [1,2,3,4]

關於Cat.prototype.constructor = Cat ,是給Cat構造函數的原型對象上的constructor屬性從新賦值。
由於任何一個prototype對象都有一個constructor屬性,指向它的構造函數,若是沒有Cat.prototype = new Animal()時,Cat.prototype.constructor是指向Cat的,可是執行了這句代碼後Cat.prototype.constructor指向Animal
至關於繼承

Cat.prototype.constructor === Animal //true

而且,構造函數創造出的每個實例也有一個constructor屬性,讀取的是構造函數的prototype對象的constructor屬性。
至關於ip

cat1.constructor === Cat.prototype.constructor // true

所以,cat1.constructor也指向Animal

cat1.constructor === Animal // true

這樣致使的結果是繼承關係混亂
手動修改了constructor,雖然解決了這個關係混亂的問題,可是代碼中也能夠看到這種實現方式也是有缺點的。
==優勢==:
實例是子類實例,同時也是父類的實例;
實例能夠訪問到父類新增的原型屬性和原型方法;
子類原型共享父類原型,父類原型不共享子類原型;

==缺點==:
繼承的實例屬性,全部子類共享同一個父類實例的實例屬性;
沒法向父類構造函數傳參;

3、原型鏈繼承改版,直接繼承prototype
基於第二種原型鏈方式的改進,想要解決以前方式的缺點。

function Animal() {
    this.age = 10
}
function Cat() {}
Animal.prototype.species = "動物"

Cat.prototype = Animal.prototype
Cat.prototype.constructor = Cat

var cat1 = new Cat()

console.log(cat1.species) // 動物
console.log(cat1.age) // undefined

Cat.prototype.gender = "formall"
var a = new Animal()
console.log(a.gender) // formall

這種方式跳過new Animal()直接繼承Animal.prototype。想象的是不共享同一個父類實例屬性,可是又致使一個問題,Cat.prototypeAnimal.prototype如今指向了同一個對象,那麼任何對Cat.prototype的修改,都會體現到Animal.prototype上。同時子類實例沒法訪問父類實例屬性。
==缺點==:
子類父類共享原型對象;
沒法繼承父類實例屬性;

4、原型鏈繼承改版,利用空對象
先來解決子類父類共享原型對象的問題,實用的辦法是建立一箇中間對象。

function Animal() { 
    this.age = 10 
}
function Cat() { }

var F = function () { }
F.prototype = Animal.prototype
Cat.prototype = new F()
Cat.prototype.constructor = Cat

Animal.prototype.species = "動物"
Cat.prototype.gender = "formall"
var cat1 = new Cat()
var a = new Animal()
console.log(cat1.species)  // 動物
console.log(cat1.age)  // undefined
console.log(a.gender) // undefined

顯然這種方式解決了子父類共享原型對象的問題,可是沒法繼承父類實例屬性的問題還在。依然不能訪問父類的實例屬性。
==優勢==:
子類添加原型屬性,父類不會更新;

==缺點==:
沒法繼承父類實例屬性;

5、組合繼承(構造函數+原型鏈)
實現了這麼多種繼承,可是每種都有缺點不足。能不能去其糟粕取其精華呢?實現一個較優的繼承方式。

function Animal(species, age) {
   this.species = species || "動物"
   this.age = age || 10
   this.list = [1,2,3]
}
function Cat() {
   Animal.call(this)
}
Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat

var c1 = new Cat('cat1')
var c2 = new Cat('cat2')
var a1 = new Animal('ani',10)

// 驗證子父類原型對象共享問題
Animal.prototype.area = "Asia"
Cat.prototype.gender = "formall"
console.log(c1.area) // Asia
console.log(a1.gender) // undeifined

// 驗證沒法訪問父類實例屬性問題
console.log(c1.species) // 動物

// 驗證不一樣實例共享父類實例屬性問題
c1.list.push(4)
console.log(c1.list) // [1,2,3,4]
console.log(c2.list) // [1,2,3]

其實繼承還有不少種實現方式,就不一一舉例了。並無最好的方式,不一樣的實現有各自的優缺點,找到最適合的就是最好的。

相關文章
相關標籤/搜索