通過之前的幾篇博客,我已經知道了.
雖然 javascript
不像傳動的 java
和 .Net
那樣,有非常完畢的繼承系統(tǒng).
但通過 JavaScript
的構造器函數對象的 .prototype
屬性,我們可以完成一些類似于繼承的操作.
補充記憶:
實例對象對原型對象的修改是COW(copy on write)
簡單的繼承體系
在javascript
中,有一種特別特殊,又被我們常常忽略掉的對象.
那么就是函數對象.
特殊之處在于,所有的函數都可以當做是構造器存在.
當使用 new 來調用這個所謂的構造器(不管這個函數是否是以構造一個對象的功能作用而聲明的).
在此函數內部都會有一個 this 關鍵字.
和普通調用函數不同的是.
當時用 new 調用函數時,情況就會非常簡單
里面的this就是構造出來的那個對象.
且這個對象默認會從構造器的 .prototype繼承屬性或者方法.
同時還有一條非常隱蔽的鏈條.
構造器的.prototype 同時也是繼承 Object.prototype 的.
function Animal (name) {
this.name = name || 'Animal'
this.sleep = function () {
console.log(this.name + ' sleep')
}
}
const cat = new Animal() // 用new調用,而不是像普通函數那樣調用.于是 this 就指向明晰了,就是構造出來的 cat 對象.
Animal.prototype.eat = function () {
console.log(this.name + ' eat')
}
cat.eat() // 所有的構造出來的對象,都會從構造它的函數的prototype上繼承.
// 一條比較隱蔽的繼承鏈(也就說所謂的原型鏈)
console.log(AnimalAnimal.prototype.__proto__ === Object.prototype) // true
一張圖
其中,畫紅色箭頭就是時常會忽略,但是為什么原型鏈為什么會這么完整的核心.
也就是為什么所有對象可以正常的調用
Object.prototype.functions
的原因.
實現(xiàn)繼承的方式一 - 原型繼承
我們都知道,如果使用new關鍵字,把一個函數當構造器來使用,那么函數構造器是會返回一個對象的.
且返回的這個對象,會從此構造器的prototype上繼承一些屬性.
而客觀存在的情況是,構造器prototype本身不是只讀的.
我們甚至可以修改覆蓋它的配置.
讓它變成一個我們希望可以繼承的對象.
比如:
function Animal() { }
const parentObject = {
name: '我是被繼承的數據',
fn () {
console.log('我是被繼承的方法')
}
}
Animal.prototype = parentObject
const a = new Animal()
console.log(a.name)
a.fn()
有了這個基本的前提之后,就開始定義我們繼承自 Animal
構造器的子類 Cat
了.
function Animal (name) {
this.name = name || 'Animal'
this.sleep = function () {
console.log(this.name + ' sleep')
}
}
Animal.prototype.eat = function () {
console.log(this.name + ' eat')
}
function Cat () { }
Cat.prototype = new Animal('狗子')
const cat = new Cat()
console.log(cat.name)
cat.sleep()
cat.eat()
原型繼承的核心就是上述代碼:Cat.prototype = new Animal('狗子')
我們讓自己定義的構造器的 prototype 對象指向父類構造器生成的對象.
由于父類構造器生成的對象包含了,父類實例定義的所有屬性以及父類構造器原型上的屬性.
所以,子類可以完整的從父類那里繼承所有的屬性.
一張圖
實現(xiàn)繼承的方式二 -- 借用函數繼承
在說明這個這種繼承方式之前,首先要稍微復習一下.
JavaScript
中 函數作為對象,它除了和普通對象一樣有 proto 屬性以外.
還有方法.
其中就有兩個比較常用的辦法 call
& apply
.
在 JavaScript
的 函數調用中.
函數從來都是不獨立調用的.
在瀏覽器環(huán)境里.
function somefn () {}
somefn()
// 等同于
someFn(window)
對于一些其他的常用的函數調用模式.
obj.method()
//
其實等同于 method(obj)
所以,函數的調用從來都不是獨立存在的.都會默認有一個隱蔽的參數.
我們可以通過 函數對象本身的 call
和 apply
來顯示的指定函數調用時的這個必備的參數是誰.
obj.method.call(obj2)
此時,在obj
里定義的函數內部訪問this
不是 obj
,而是 obj2
了.
有了上述復習.
可以開始寫構造器繼承了.
首先定義一個基類
function Animal (name) {
this.name = name || 'Animal'
this.sleep = function () {
console.log(this.name + ' sleep')
}
}
然后定義子類 Cat
function Cat (name) {
Animal(this, name)
}
關鍵一句是在 Animal.call(this,name)
雖然,之前,我們都把 Animal
當成構造器存在,要使用new關鍵字來調用.
但是在這里,我們把 Animal當成普通函數而非構造器.
利用普通函數的 call
方法,改變 this
..
const cat = new Cat('葫蘆娃')
console.log(cat.name)
cat.sleep()
這里的 this 是由 new Cat('葫蘆娃')
來創(chuàng)建的,所以就表明了是 cat
的一個實例.
結果:
這種繼承方式有一個違反直覺的缺點:
既然我們本意是讓
Cat
繼承自Animal
我們當然也希望Cat
能當做原型繼承那樣能夠正常的調用Animal.prototype
上的方法.
但這種方式不行.
Animal本來是個構造函數.
但是由于,借用函數繼承,把它當成了一個普通的函數來使用.(調用.call
方法)
所以 new Cat()
對象,無法調用Animal函數定義在 prototype 上的屬性和方法.
function Animal (name) {
this.name = name || 'Animal'
this.sleep = function () {
console.log(this.name + ' sleep')
}
}
Animal.prototype.run = function () {
console.log(`${this.name} run!`)
}
function Cat (name) {
Animal.call(this, name)
}
const cat = new Cat('葫蘆娃')
console.log(cat.name) // 沒問題
cat.sleep() // 沒問題
cat.run()// cat.run is not a function
一張圖
紅色的路徑,壓根就不在 Cat
的原型繼承鏈條中,所以就無法使用到 Animal.prototype
上的屬性和方法了.
實現(xiàn)繼承的方式三 -- 組合繼承
組合繼承,組合的是:
- 原型鏈繼承
- 借用函數繼承
這種方式的做法,是為了解決:
借用函數構造方法,無法使用函數原型上的屬性和方法而產生的.
function Animal (name) {
this.name = name
this.eat = function () {
console.log(`${this.name} eat`)
}
}
Animal.prototype.run = function () {
console.log(`${this.name} run`)
}
function Cat (name) {
// 實例數據繼承到了. name,eat()
Animal.call(this,name)
}
// 原型數據繼承到了 run()
// 原型數據繼承到了 run()
Cat.prototype = new Animal('??') // 這樣寫,會造成兩次Animal實例化.且沒有自己的原型了.
Cat.prototype = Animal.prototype // 這樣寫,不會造成兩次Animal實例化,且沒有自己的原型了.
const cat = new Cat('??')
cat.eat()
cat.run()
結果
- 使用
Animal.call()
來繼承Animal
的實例屬性和方法. - 使用
Cat.prototype = Animal.prototype
來使用Animal.prototype
屬性和方法. 這樣避免了兩次調用Animal
構造函數,但是Cat
沒有自己的原型prototype
- 使用
Cat.prototype = new Animal()
會造成兩次構造函數調用.第一次new Animal()
,第二次:Animal.call(this,name)
,同樣的讓Cat
也棄用了自己的原型prototype
實現(xiàn)繼承的方式四 -- 原型式繼承
原型式繼承的核心,其實很簡單.
需要提供一個被繼承的對象.(這里不是函數,而是是實實在在的對象)
然后把這個對象掛在到某個構造函數的prototype上.
此時,如果我們使用這個構造函數的new,就可以創(chuàng)建出一個對象.
這個對象就繼承了上述提供的實實在在對象上的屬性和方法了.
function inherit (obj) {
function Constructor () { } // 提供一個函數
Constructor.prototype = obj // 設置函數的 prototype
return new Constructor() // 返回這個函數實例化出來的對象.
}
function Animal (name) {
this.name = name
this.eat = function () {
console.log(`${this.name} eat`)
}
}
Animal.prototype.run = function () {
console.log(`${this.name} run`)
}
const animal = new Animal('小貓')
const cat = inherit(animal) // cat 要從animal對象上繼承它所有的方法和屬性.
cat.eat()
cat.run()
結果:
這種繼承方式,就是可以創(chuàng)建出一個繼承自某個對象的對象.
Object.create 方法內部差不多也是這么一個實現(xiàn)原理.
const cat2 = Object.create(animal, {
food: {
writable: true,
enumerable: true,
configurable: true,
value: '小魚干'
}
}) // cat2 對象從 animal 對象上繼承. 并擴展自己一個food屬性.
cat2.name = '小貓2'
console.log(cat2.food)
cat2.run()
cat2.eat()
從一個對象繼承,而不是類.
弱化的類的概念.
實現(xiàn)繼承的方式五 -- 寄生式繼承
寄生?
寄生誰?
就是把上述的 inherit
函數在包裝一下.
function inherit (obj) {
if (typeof obj !== 'object') throw new Error('必須傳入一個對象')
function Constructor () { }
Constructor.prototype = obj
return new Constructor()
}
function createSubObj (superObject, options) {
var clone = inherit(superObject)
if (options && typeof options === 'object') {
Object.assign(clone, options)
}
return clone
}
const superObject = {
name: '張三',
age: 22,
speak () {
console.log(`i am ${this.name} and ${this.age} years old!`)
}
}
const subObject = createSubObj(superObject, {
professional: '前端工程師',
report : function () {
console.log(`i am a ${this.professional}`)
}
})
subObject.speak()
subObject.report()
結果:
仍然沒有class
的概念. 依然是從對象上繼承.
包裝起來的意義在哪?
僅僅只是包裝起來了而已...可以漸進增加一下對象的感覺????
實現(xiàn)繼承的方式六 - 寄生組合式繼承
上面講述的 原型式繼承 和 寄生式繼承
都是對象在參與,弱化了類的概念.
而繼承應該是由類來參與的.(之類說的的類來參與指的是讓構造函數的prototype
來參與)
所以,寄生組合式繼承還是讓類來參與繼承.
function inheritPrototype (SuperType, SubType) {
if (typeof SuperType !== 'function' || typeof SubType !== 'function') {
throw new Error('必須傳遞構造函數!')
}
// 這個地方利用Object.create(Subtype.prototype)
// 非常巧妙的讓Subtype.prototype對象繼承自 SuperType.prototype.
// 而不是去覆蓋自己.
// 特別注意:!!!!!!!!!!!!! Object.create 方法會返回一個對象 obj. obj.__proto__ = Object.create 函數接受的參數.
// 所以,任何在此代碼前給 obj 設置的屬性和方法,都應該在此方法執(zhí)行完畢之后在執(zhí)行,否則會被覆蓋.
// 引用都變了,當然會時效.
SubType.prototype = Object.create(SuperType.prototype)
}
inheritPrototype(SuperType, SubType)
function SuperType (name) {
this.name = name
this.showName = function () {
console.log('from SuperType:' + this.name)
}
}
SuperType.prototype.super_protoProperty = 'SuperType原型屬性'
SuperType.prototype.super_protoFunction = function () {
console.log('SuperType原型方法')
}
function SubType (name, age) {
SuperType.call(this, name)
this.age = age
this.showAge = function () {
console.log('from SubType:' + this.age)
}
}
SubType.prototype.sub_protoProperty = 'SubType原型屬性'
SubType.prototype.sub_protoFunction = function () {
console.log('SunType原型方法')
}
const sub = new SubType('張三', 22)
sub.showAge()
sub.showName()
console.log(sub.super_protoProperty) // 拿不到 undefined
sub.super_protoFunction() // 方法不存在.
sub.sub_protoFunction() // 拿自己的原型沒問題
console.log(sub.sub_protoProperty) // 拿自己的原型沒問題
核心代碼就是上述的
SubType.prototype = Object.create(SuperType.prototype)
這句代碼利用 Object.create()
方法,非常巧妙的讓
SubType.prototype
繼承 SuperType.prototype
這兒做: SubType
既保留了自己的原型對象.又能從 SuperType
的原型上繼承.
運行結果:
from SubType:22
from SuperType:張三
SuperType原型屬性
SuperType原型方法
SunType原型方法
SubType原型屬性
這樣做法的好處非常明顯.
子類不光可以從父類繼承實例屬性.(SubType.call(this).
還能從父類的原型繼承屬性 (SubType.prototype = Object.create(SubperType.prototype)
一張圖
-
SubType
從SuperType.call(this)
繼承到了SuperType
的實例屬性. -
SubType
在new SubType
里聲明了自己的屬性. - 由于
Subtype.prototype
不是想原型組合集成那樣是覆蓋自己的原型,而是讓原型對象繼承子SuperType.prototype
. - 所以
SubType.prototype
原型對象仍然存在.所以SubType
可以從自己的原型上繼承. - 同時
Subtype.prototype
:SuperType.prototype
. 所以,Subtype
還可以從SuperType.prototype
上繼承屬性.
new SubType()
- 自己的實例屬性 --> bingo
- 自己的原型對象 ---> bingo
- 父類的實例屬性 ---> bingo
- 父類的原型對象 ---> bingo