記不住的繼承方式
都說程序員是這個世界上最懶的人, 能躺著絕不坐著, 全乾著複製黏貼的活.
『什麼, 你說這套邏輯之前寫過?!?! 速速把代碼呈上來!!!』.最懶的人往往信奉著『拿來主義』. 若只是簡單的複製黏貼, 就會顯得沒有逼格.
在 JavaScript 中, 重複用到的邏輯我們會用函數包裝起來, 在合適且需要的情況下, 調用該函數即可. 而 apply, call, new 等方法也拓寬了函數的使用場景.
除了這種借來的, 我們還有繼承來的. 這就是常說的原型繼承. 當對象本身沒有要查詢的屬性或方法時, 它會沿著原型鏈查找, 找到了就會拿來使用. 這種無中生有的事, 不妨瞭解一下.
預備知識
- 默認情況下, 所有的原型對象都會自動獲得一個 constructor (構造函數)屬性, 這個屬性是一個指向 prototype 屬性所在函數的指針. 構造函數的原型 prototype 上 constructor 的初始值是構造函數本身. 即,
Function.prototype.constructor === Function // true
- 由構造函數構造出來的實例本身沒有 constructor 屬性, 不過可以通過原型鏈繼承這個屬性.
// 以下person的constructor屬性繼承自Person.prototype
function Person() {} Person.prototype.constructor === Person // true
let person = new Person(); person.constructor === Person // true
person.hasOwnProperty(constructor) === false // true
person_1.constructor === Person.prototype.constructor // true
- 簡單數據類型和複雜數據類型賦值傳參的區別.
JavaScript 中變數不可能成為只想另一個變數的引用. 引用指向的是值. 複雜數據類型的引用指向的都是同一個值.它們相互之間沒有引用/指向關係. 一旦值發生變化, 指向該值的多個引用將共享這個變化.
- new, apply, call 的函數調用模式.三者的共同點都是都是指定調用函數的 this 值. 這使得同一個函數可以在不同的語境下正確執行. new 更為複雜一些. 可大致模擬為,
function new(constructor, arguments) {
let instance = Object.create(constructor.prototype) // 姑且稱之為 new 的特性一
constructor.apply(instance, arguments) // 姑且稱之為 new 的特性二
return instance
}
很明顯, new 的操作中包涵了 apply, call 要做的事. 在此大膽猜測一下, 在實現繼承的過程中, 一旦同時出現 new 和 apply或 call, 就會有重複交集的可能, 這時就需要想想是否有可以改進的地方.
不著痕跡的拿來主義
各單位請注意, 下面到我表演的時候了
上道具!
function Animal(name) {
this.name = name
}
Animal.prototype.species = animal
function Leo() {} // 我是頭小獅子
想要無中生有, 那是不可能的??, 所以我們準備了模板 Animal. Animal 有的東西, Leo 也想擁有.
而且 Animal 能用地東西也同樣適用於 Leo. 所以, 我們期待 Leo 最終長成這個樣子.
function Leo(name) {
this.name = name
}
Leo.prototype.species = animal
就長這副熊樣!? 這和簡單的複製黏貼有什麼區別!? 這和鹹魚又有什麼區別!? 說好的逼格呢!?
觀察一下 Leo, Leo 構造函數內部邏輯和 Animal 構造函數的內部邏輯如出一轍. 既然都是一樣的, 為什麼不能借來用用呢? 改造一下,
function Animal(name) {
this.name = name
}
Animal.prototype.species = animal
function Leo(name) {
Animal.call(this, name)
}
這種在構造函數內部借函數而不藉助原型繼承的方式被稱之為 借用構造函數式繼承.
把屬性和方法放在構造函數內部的定義, 使得每個構造出來的實例都有自己的屬性和方法. 而對一些需要實例間共享的屬性或方法卻是沒轍.
當然了, 我們本來就沒打算止步於此. 構造函數內部可以靠借, 那原型上呢? 如何讓 Leo 的原型上能和 Animal 的原型保持一致呢?
這不是廢話麼? 我除了會借, 我還會繼承啊, 原型繼承啊!!!
關於原型鏈, 我們已經知道是怎麼一回事了(不知道的可參考從Function入手原型鏈).
原型繼承就是通過原型鏈實現了對象本身沒有的屬性訪問和方法調用. 利用這個特性, 我們可以在原型上做些手腳.
思路一: 可以使得 Leo 的 prototype 直接指向 Animal 的 prototype.
function Animal(name) {
this.name = name
}
Animal.prototype.species = animal
function Leo(name) {
Animal.call(this, name)
}
Leo.prototype = Animal.prototype
這裡有一點需要注意的, Leo.prototype = Animal.prototype
這種寫法就等於完全覆寫了 Leo 的原型, Leo.prototype.constructor
將和 Animal.prototype.constructor
保持一致, 這會使得一些等式顯得詭異.
不信, 請看:
Leo.prototype.constructor === Animal.prototype.constructor === Animal
針對這種情況, 我們往往會做一些修正:
// 接上例代碼省略
Leo.prototype = Animal.prototype
Leo.prototype.constructor = Leo
即使修正好了, 可是還有個大問題.
那就是, 如果想給 Leo 原型添加屬性或方法, 將會影響到 Animal, 進而會影響到所有 Animal 的實例. 畢竟它們的原型之間已經畫了等號.
// 接上例代碼省略
let Dog = new Animal(dog)
Dog.sayName // undefined
Leo.prototype.sayName = function() {
console.log(this.name)
}
Dog.sayName() // dog
我只想偷個懶, 沒想過要搗亂啊??!!!
為了消除這種影響, 我們需要一個中間紐帶過渡. 還好我們知道 new 可以用來修改原型鏈.
思路二: Leo 的 prototype 指向 Animal 的實例.
function Animal(name) {
this.name = name
}
Animal.prototype.species = animal
function Leo(name) {
Animal.call(this, name)
}
Leo.prototype = new Animal()
Leo.prototype.contructor = Leo
這種在構造函數內部借函數同時又藉助原型繼承的方式被稱之為 組合繼承. Leo 換個角度其實長這樣:
function Leo(name) {
this.name = name
}
Leo.prototype = {
constructor: Leo,
name: undefined,
__proto__: Animal.prototype
}
在這種繼承模式中, Leo 的實例可以有自己的屬性和方法, 實例之間又可以通過 prototype 來共享屬性和方法卻不會影響 Animal, 還可以通過 _proto_
追溯到 Animal.prototype.
一切都很完美??. 不過還記得文章開始時所說的麼
在實現繼承的過程中, 一旦同時出現 new 和 apply 或 call, 就會有重複交集的可能, 這時就需要想想是否有可以改進的地方.
Animal 被調用了兩次, 第一次是 Leo 構造函數內部作為一個普通函數被調用, 第二次是被作為構造函數構造一個實例充當 Leo的原型.
Animal 內部定義的屬性和方法同時出現在 Leo 的原型和 Leo 的實例上. 實例上有的東西就不會再到原型上查找. 反之, 實例上沒有的東西才會到原型上查找. 顯然, 有多餘的存在.
這不是最優解, 我要最好的! 下一個!
思路三: 既然有重複, 那就去其一唄. 既然 new 比 call 和 apply 厲害, 那就留著 new 吧.
function Animal(name) {
this.name = name
}
Animal.prototype.species = animal
function Leo(name) {}
Leo.prototype = new Animal()
Leo.prototype.contructor = Leo
這種在構造函數內部不借函數只藉助原型繼承的方式被稱之為 原型鏈繼承.
經過這麼一折騰, 發現不好的地方有增無減. 實例沒了自己的屬性和方法了, 連 Animal 構造函數內部定義的屬性方法都可以在實例間共享了(思路二也存在這個問題), 而且參數也不給傳了.
我要的不多, 能輕點折騰不, 心臟不好
回到 思路二, 那就刪了 new 吧.
思路四: 接上 思路二, 刪了 new, 那隻能在原型上做調整了.
我們從一開始就只是希望 Leo 的 prototype 指向 Animal 的 prototype, 不多不少且不會出現 思路一 的壞影響.
既然不能直接在兩者之間畫等號, 就造一個過渡紐帶唄. 能夠關聯起原型鏈的不只有 new, Object.create() 也是可以的.
創建一個 _proto_
指向 Animal.prototype 的對象充當 Leo 的原型不就解決問題了麼.
function Animal(name) {
this.name = name
}
Animal.prototype.species = animal
function Leo(name) {
Animal.call(this, name)
}
Leo.prototype = Object.create(Animal.prototype)
Leo.prototype.contructor = Leo
這種在構造函數內部借函數同時又間接藉助原型繼承的方式被稱之為 寄生組合式繼承.
這種模式完美解決了 思路二 的弊端. 算是較為理想的繼承模式吧.
確認過眼神, 你才我想要的!
以上還是隻是構造函數間的繼承, 還有基於已存在對象的繼承, 譬如, 原型式繼承 和 寄生式繼承等.
講真, 說了辣麼多, 我還真沒記住 借用構造函數式繼承, 組合繼承, 原型鏈繼承, 寄生組合式繼承, 原型式繼承, 寄生式繼承等.
你沒記住這麼多模式, 那你都記住什麼了
答曰: 要想很好得繼承, 一靠朋友, 二靠拼爹.
這孩子是不是傻? 這都什麼年代了? 再說了, 就沒人告訴你你家裡有礦???
思路五: ES6 引入了 Class(類)這個概念,通過 class 關鍵字,可以定義類, Class 實質上是 JavaScript 現有的基於原型的繼承的語法糖. Class 可以通過extends關鍵字實現繼承. 我們可以對 思路四 來個華麗變身.
class Animal {
constructor(name) {
this.name = name
}
}
Animal.prototype.species = animal
class Leo extends Animal {
constructor(name) {
super(name)
}
}
經過這麼一處理後行為上和 思路四 基本沒什麼區別, constructor(){}
充當了之前的構造函數, super()
作為函數調用扮演著 Animal.call(this, name)
的角色(還可以表示父類). 最重要的是 Leo 的 _proto_
也指向了 Animal.
礦多基因好, 嘖嘖嘖, 我都快要喜歡上我自己了??.
推薦閱讀: