JavaScript面向對象詳解(三)

JavaScript面向對象詳解(三)

Original coderwhy coderwhy Yesterday
此文爲轉載

繼承是面向對象中非常重要的特性.

ES5中和類的實現一樣, 不能直接實現繼承. 實現繼承主要是依靠原型鏈來實現的。

一. 原型鏈

原型鏈是ES5中實現繼承的主要手段, 因此相對比較重要, 我們需要深入理解原型鏈.

1.1. 深入理解原型鏈

先來回顧一下構造函數、原型和實例的關係:

  • 每個構造函數都有一個原型對象, 通過prototype指針指向該原型對象.

  • 原型對象都包含一個指向構造函數的指針, 通過constructor指針, 指向構造函數

  • 而實例都包含一個指向原型對象的內部指針, 該內部指針我們通常使用__proto__來描述.

思考如下情況:

  • 我們知道, 可以通過Person.prototype = {}的方式來重寫原型對象.

  • 假如, 我們後面賦值的不是一個{}, 而是另外一個類型的實例, 結果會是怎麼樣呢?

  • 顯然,此時的原型對象將包含一個指向另一個原型的指針,相應地,另一個原型中也包含着一個指向另一個構造函數的指針。

  • 假如另一個原型又是另一個類型的實例,那麼上述關係依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂原型鏈的基本概念。

有些抽象, 我們通過代碼來理解:

// 創建Person構造函數
function Person() {
}

// 設置Animal的原型
Person.prototype = {
}

我們將代碼修改成原型鏈的形式:

// 1.創建Animal的構造函數
function Animal() {
    this.animalProperty = "Animal"
}

// 2.給Animal的原型中添加一個方法
Animal.prototype.animalFunction = function () {
    alert(this.animalProperty)
}

// 3.創建Person的構造函數
function Person() {
    this.personProperty = "Person"
}

// 4.給Person的原型對象重新賦值
Person.prototype = new Animal()

// 5.給Person添加屬於自己的方法
Person.prototype.personFunction = function () {
    alert(this.personProperty)
}

// 6.創建Person的實例
var person = new Person()
person.animalFunction()
person.personFunction()

代碼解析:

  • 代碼有一些複雜, 但是如果你希望學習好原型鏈, 必須耐心去看一看上面的代碼, 你會發現其實都是我們學習過的.

  • 重點我們來看第4步代碼: 給Person.prototype賦值了一個Animal的實例. 也就是Person的原型變成了Animal的實例.

  • Animal實例本身有一個__proto__可以指向Animal的原型.

  • 那麼, 我們來思考一個問題: 如果現在搜索一個屬性或者方法, 這個時候會按照什麼順序搜索呢?

    • 第一步, 在person實例中搜索, 搜索到直接返回或者調用函數. 如果沒有執行第二步.

    • 第二步, 在Person的原型中搜索, Person的原型是誰? Animal的實例. 所以會在Animal的實例中搜索, 無論是屬性還是方法, 如果搜索到則直接返回或者執行. 如果沒有, 執行第三步.

    • 第三步, 在Animal的原型中搜索, 搜索到返回或者執行, 如果沒有, 搜索結束. (當然其實還有Object, 但是先不考慮)

畫圖解析可能更加清晰:

當代碼執行到第3步(上面代碼的序號)的時候, 如圖所示:

img

當代碼執行第4步(上面代碼的序號)時, 發生瞭如圖所示的變化

  • 注意圖片中的紅色線, 原來指向的是誰, 現在指向的是誰.

img

代碼繼續執行

  • Person.prototype.personFunction = function (){}

  • 當執行第5步, 也就是給Person的原型賦值了一個函數時, 事實上在給new Animal(Animal的實例)賦值了一個新的方法.

img

代碼繼續執行, 我們創建了一個Person對象

  • 創建Person對象, person對象會有自己的屬性, personProperty.

  • 另外, person對象有一個__prototype__指向Person的原型.

  • Person的原型是誰呢? 就是我們之前的new Animal(Animal的一個實例), 所以會指向它.

原型鏈簡單總結:

  • 通過實現原型鏈,本質上擴展了本章前面介紹的原型搜索機制。

  • 當以讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性。如果沒有找到該屬性,則會繼續搜索實例的原型。在通過原型鏈實現繼承的情況下,搜索過程就得以沿着原型鏈繼續向上。

  • 在找不到屬性或方法的情況下,搜索過程總是要一環一環地前行到原型鏈末端纔會停下來。

1.2. 原型和實例的關係

如果我們希望確定原型和實例之間的關係, 有兩種方式:

  • 第一種方式是使用instanceof操作符,只要用這個操作符來測試實例與原型鏈中出現過的構造函數,結果就會返回true。

  • 第二種方式是使用isPrototypeOf()方法。同樣,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型,因此isPrototypeOf()方法也會返回true

instanceof操作符

// instanceof
alert(person instanceof Object) // true
alert(person instanceof Animal) // true
alert(person instanceof Person) // true

isPrototypeOf()函數

// isPrototypeOf函數
alert("isPrototypeOf函數函數")
alert(Object.prototype.isPrototypeOf(person)) // true
alert(Animal.prototype.isPrototypeOf(person)) // true
alert(Person.prototype.isPrototypeOf(person)) // true

1.3. 添加新的方法

添加新的方法

  • 在第5步操作中, 我們爲子類型添加了一個新的方法. 但是這裏有一個注意點.

  • 無論是子類中添加新的方法, 還是對父類中方法進行重寫. 都一定要將添加方法的代碼, 放在替換原型語句之後.

  • 否則, 我們添加的方法將會無效.

錯誤代碼引起的代碼:

// 1.定義Animal的構造函數
function Animal() {
    this.animalProperty = "Animal"
}

// 2.給Animal添加方法
Animal.prototype.animalFunction = function () {
    alert(this.animalProperty)
}

// 3.定義Person的構造函數
function Person() {
    this.personProperty = "Person"
}

// 4.給Person添加方法
Person.prototype.personFunction = function () {
    alert(this.personProperty)
}

// 5.給Person賦值新的原型對象
Person.prototype = new Animal()

// 6.創建Person對象, 並且調用方法
var person = new Person()
person.personFunction() // 不會有任何彈窗, 因爲找不到該方法

代碼解析:

  • 執行上面的代碼不會出現任何的彈窗, 因爲我們添加的方法是無效的, 被賦值的新的原型覆蓋了.

  • 正確的辦法是將第4步和第5步操作換一下位置即可.

總結

  • 其實這個問題沒什麼好說的, 只要你理解了原型鏈(好好看看我上面畫的圖, 或者自己畫一下圖)

  • 但是, 切記在看圖的過程中一樣掃過, 因爲這會讓你錯過很多細節, 對原型鏈的理解就會出現問題.

1.4. 原型鏈的問題

原型鏈對於繼承來說:

  • 原型鏈似乎對初學JavaScript原型的人來說, 已經算是比較高明的設計技巧了, 有些人理解起來都稍微有些麻煩.

  • 但是, 這種設計還存在一些缺陷, 不是最理性的解決方案. (但是後續的解決方案也是依賴原型鏈, 無論如何都需要先理解它)

原型鏈存在的問題:

  • 原型鏈存在最大的問題是關於引用類型的屬性.

  • 通過上面的原型實現了繼承後, 子類的person對象繼承了(可以訪問)Animal實例中的屬性(animalProperty).

  • 但是如果這個屬性是一個引用類型(比如數組或者其他引用類型), 就會出現問題.

引用類型的問題代碼:

// 1.定義Animal的構造函數
function Animal() {
    this.colors = ["red", "green"]
}

// 2.給Animal添加方法
Animal.prototype.animalFunction = function () {
    alert(this.colors)
}

// 3.定義Person的構造函數
function Person() {
    this.personProperty = "Person"
}

// 4.給Person賦值新的原型對象
Person.prototype = new Animal()

// 5.給Person添加方法
Person.prototype.personFunction = function () {
    alert(this.personProperty)
}

// 6.創建Person對象, 並且調用方法
var person1 = new Person()
var person2 = new Person()

alert(person1.colors) // red,green
alert(person2.colors) // red,green

person1.colors.push("blue")

alert(person1.colors) // red,green,blue
alert(person2.colors) // red,green,blue

代碼解析:

  • 我們查看第6步的操作

  • 創建了兩個對象, 並且查看了它們的colors屬性

  • 修改了person1中的colors屬性, 添加了一個新的顏色blue

  • 再次查看兩個對象的colors屬性, 會發現person2的colors屬性也發生了變化

  • 兩個實例應該是相互獨立的, 這樣的變化如果我們不制止將會在代碼中引發一些列問題.

原型鏈的其他問題:

  • 在創建子類型的實例時,不能向父類型的構造函數中傳遞參數。

  • 實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給父類型的構造函數傳遞參數。

  • 從而可以修改父類型中屬性的值, 在創建構造函數的時候就確定一個值.

二. 經典繼承

爲了解決原型鏈繼承中存在的問題, 開發人員提供了一種新的技術: constructor stealing(有很多名稱: 借用構造函數或經典繼承或僞造對象), steal是偷竊的意思, 但是這裏可以翻譯成借用.

2.1. 經典繼承的思想

經典繼承的做法非常簡單: 在子類型構造函數的內部調用父類型構造函數.

  • 因爲函數可以在任意的時刻被調用

  • 因此通過apply()和call()方法也可以在新創建的對象上執行構造函數.

經典繼承代碼如下:

// 創建Animal的構造函數
function Animal() {
    this.colors = ["red", "green"]
}

// 創建Person的構造函數
function Person() {
    // 繼承Animal的屬性
    Animal.call(this)

    // 給自己的屬性賦值
    this.name = "Coderwhy"
}

// 創建Person對象
var person1 = new Person()
var person2 = new Person()

alert(person1.colors) // red,greem
alert(person2.colors) // red,greem
person1.colors.push("blue")
alert(person1.colors) // red,green,blue
alert(person2.colors) // red,green

代碼解析:

  • 我們通過在Person構造函數中, 使用call函數, 將this傳遞進去.

  • 這個時候, 當Animal中有相關屬性初始化時, 就會在this對象上進行初始化操作.

  • 這樣就實現了類似於繼承Animal屬性的效果.

這個時候, 我們也可以傳遞參數, 修改上面的代碼:

// 創建Animal構造函數
function Animal(age) {
    this.age = age
}

// 創建Person構造函數
function Person(name, age) {
    Animal.call(this, age)
    this.name = name
}

// 創建Person對象
var person = new Person("Coderwhy", 18)
alert(person.name)
alert(person.age)

2.2. 經典繼承的問題

經典繼承的問題:

  • 對於經典繼承理解比較深入, 你已經能發現: 經典繼承只有屬性的繼承, 無法實現方法的繼承.

  • 因爲調用call函數, 將this傳遞進去, 只能將父構造函數中的屬性初始化到this中.

  • 但是如果函數存在於父構造函數的原型對象中, this中是不會有對應的方法的.

回顧原型鏈和經典繼承:

  • 原型鏈存在的問題是引用類型問題和無法傳遞參數, 但是方法可以被繼承

  • 經典繼承是引用類型沒有問題, 也可以傳遞參數, 但是方法無法被繼承.

  • 怎麼辦呢? 將兩者結合起來怎麼樣?

三. 組合繼承

如果你認識清楚了上面兩種實現繼承的方式存在的問題, 就可以很好的理解組合繼承了.

組合繼承(combination inheritance, 有時候也稱爲僞經典繼承), 就是將原型鏈和經典繼承組合在一起, 從而發揮各自的優點.

3.1. 組合繼承的思想

組合繼承:

  • 組合繼承就是發揮原型鏈和經典繼承各自的優點來完成繼承的實現.

  • 使用原型鏈實現對原型屬性和方法的繼承.

  • 通過經典繼承實現對實例屬性的繼承, 以及可以在構造函數中傳遞參數.

組合繼承的代碼:

// 1.創建構造函數的階段
// 1.1.創建Animal的構造函數
function Animal(age) {
    this.age = age
    this.colors = ["red", "green"]
}

// 1.2.給Animal添加方法
Animal.prototype.animalFunction = function () {
    alert("Hello Animal")
}

// 1.3.創建Person的構造函數
function Person(name, age) {
    Animal.call(this, age)
    this.name = name
}

// 1.4.給Person的原型對象重新賦值
Person.prototype = new Animal(0)

// 1.5.給Person添加方法
Person.prototype.personFunction = function () {
    alert("Hello Person")
}

// 2.驗證和使用的代碼
// 2.1.創建Person對象
var person1 = new Person("Coderwhy", 18)
var person2 = new Person("Kobe", 30)

// 2.2.驗證屬性
alert(person1.name + "-" + person1.age) // Coderwhy,18
alert(person2.name + "-" + person2.age) // Kobe,30

// 2.3.驗證方法的調用
person1.animalFunction() // Hello Animal
person1.personFunction() // Hello Person

// 2.4.驗證引用屬性的問題
person1.colors.push("blue")
alert(person1.colors) // red,green,blue
alert(person2.colors) // red,green

代碼解析:

  • 根據前面學習的知識, 結合當前的代碼, 大家應該可以理解上述代碼的含義.

  • 但是我還是建議大家一定要多手動自己來敲代碼, 來理解其中每一個步驟.

  • 記住: 看懂, 聽懂不一定真的懂, 自己可以寫出來, 纔是真的懂了.

3.2. 組合繼承的分析

組合繼承是JavaScript最常用的繼承模式之一.

  • 如果你理解到這裏, 點到爲止, 那麼組合來實現繼承只能說問題不大.

  • 但是它依然不是很完美, 存在一些問題不大的問題.(不成問題的問題, 基本一詞基本可用, 但基本不用)

組合繼承存在什麼問題呢?

  • 組合繼承最大的問題就是無論在什麼情況下, 都會調用兩次父類構造函數.

  • 一次在創建子類原型的時候

  • 另一次在子類構造函數內部(也就是每次創建子類實例的時候).

  • 另外, 如果你仔細按照我的流程走了上面的每一個步驟, 你會發現: 所有的子類實例事實上會擁有兩份父類的屬性

  • 一份在當前的實例自己裏面(也就是person本身的), 另一份在子類對應的原型對象中(也就是person.__proto__裏面)

  • 當然, 這兩份屬性我們無需擔心訪問出現問題, 因爲默認一定是訪問實例本身這一部分的.

怎麼解決呢?

  • 看起來組合繼承也不是非常完美的解決方案, 雖然也可以應用.

  • 有沒有終極的解決方案呢? 預知後事如何, 且聽下回分解.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章